import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/services.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:photo_view/photo_view_gallery.dart'; import 'package:pixes/components/md.dart'; import 'package:pixes/components/message.dart'; import 'package:pixes/components/page_route.dart'; import 'package:pixes/foundation/app.dart'; import 'package:pixes/foundation/cache_manager.dart'; import 'package:pixes/foundation/image_provider.dart'; import 'package:pixes/pages/main_page.dart'; import 'package:pixes/utils/io.dart'; import 'package:pixes/utils/translation.dart'; import 'package:share_plus/share_plus.dart'; import 'package:window_manager/window_manager.dart'; class ImagePage extends StatefulWidget { const ImagePage(this.urls, {this.initialPage = 0, super.key}); final List urls; final int initialPage; static show(List urls, {int initialPage = 0}) { App.rootNavigatorKey.currentState?.push(AppPageRoute( builder: (context) => ImagePage(urls, initialPage: initialPage))); } @override State createState() => _ImagePageState(); } class _ImagePageState extends State with WindowListener { int windowButtonKey = 0; @override void initState() { windowManager.addListener(this); super.initState(); } @override void dispose() { windowManager.removeListener(this); super.dispose(); } @override void onWindowMaximize() { setState(() { windowButtonKey++; }); } @override void onWindowUnmaximize() { setState(() { windowButtonKey++; }); } late var controller = PageController(initialPage: widget.initialPage); late int currentPage = widget.initialPage; var menuController = FlyoutController(); Future getFile() async { var image = widget.urls[currentPage]; if (image.startsWith("file://")) { return File(image.replaceFirst("file://", "")); } var key = image; if (key.startsWith("novel:")) { key = key.split(':').last; } var file = await CacheManager().findCache(key); return file == null ? null : File(file); } String getExtensionName() { var fileName = widget.urls[currentPage].split('/').last; if (fileName.contains('.')) { return '.${fileName.split('.').last}'; } return '.jpg'; } void showMenu() { menuController.showFlyout( barrierColor: Colors.transparent, position: App.isMobile ? Offset(context.size!.width, 0) : null, builder: (context) => MenuFlyout( items: [ MenuFlyoutItem( text: Text("Save to".tl), onPressed: () async { var file = await getFile(); if (file != null) { var fileName = widget.urls[currentPage].split('/').last; if (!fileName.contains('.')) { fileName += getExtensionName(); } saveFile(file, fileName); } }), if (App.isMobile) MenuFlyoutItem( text: Text("Save to gallery".tl), onPressed: () async { var file = await getFile(); if (file != null) { var fileName = widget.urls[currentPage].split('/').last; if (!fileName.contains('.')) { fileName += getExtensionName(); } await ImageGallerySaver.saveImage( await file.readAsBytes(), quality: 100, name: fileName); if (mounted) { showToast(context, message: "Saved".tl); } } }), MenuFlyoutItem( text: Text("Share".tl), onPressed: () async { var file = await getFile(); if (file != null) { var ext = getExtensionName(); var fileName = widget.urls[currentPage].split('/').last; if (!fileName.contains('.')) { fileName += ext; } var mediaType = switch (ext) { '.jpg' => 'image/jpeg', '.jpeg' => 'image/jpeg', '.png' => 'image/png', '.gif' => 'image/gif', '.webp' => 'image/webp', _ => 'application/octet-stream' }; Share.shareXFiles([ XFile.fromData(await file.readAsBytes(), mimeType: mediaType, name: fileName) ]); } }), ], )); } @override Widget build(BuildContext context) { return Container( padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), color: FluentTheme.of(context).micaBackgroundColor, child: Listener( onPointerSignal: (event) { if (event is PointerScrollEvent && !HardwareKeyboard.instance.isControlPressed) { if (event.scrollDelta.dy > 0 && controller.page!.toInt() < widget.urls.length - 1) { controller.jumpToPage(controller.page!.toInt() + 1); } else if (event.scrollDelta.dy < 0 && controller.page!.toInt() > 0) { controller.jumpToPage(controller.page!.toInt() - 1); } } }, child: LayoutBuilder( builder: (context, constrains) { var height = constrains.maxHeight; return Stack( children: [ Positioned.fill( child: PhotoViewGallery.builder( pageController: controller, backgroundDecoration: const BoxDecoration(color: Colors.transparent), itemCount: widget.urls.length, builder: (context, index) { var image = widget.urls[index]; return PhotoViewGalleryPageOptions( filterQuality: FilterQuality.medium, imageProvider: getImageProvider(image), ); }, onPageChanged: (index) { setState(() { currentPage = index; }); }, )), Positioned( top: 0, left: 0, right: 0, child: SizedBox( height: 36, child: Row( children: [ const SizedBox( width: 6, ), IconButton( icon: const Icon(FluentIcons.back).paddingAll(2), onPressed: () => context.pop()), const Expanded( child: DragToMoveArea( child: SizedBox.expand(), ), ), buildActions(), if (App.isDesktop) WindowButtons( key: ValueKey(windowButtonKey), ), ], ), ), ), if (currentPage != 0) Positioned( left: 0, top: height / 2 - 9, child: IconButton( icon: const Icon( FluentIcons.chevron_left, size: 18, ), onPressed: () { controller.previousPage( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, ).paddingAll(8), ), if (currentPage != widget.urls.length - 1) Positioned( right: 0, top: height / 2 - 9, child: IconButton( icon: const Icon(FluentIcons.chevron_right, size: 18), onPressed: () { controller.nextPage( duration: const Duration(milliseconds: 300), curve: Curves.easeInOut, ); }, ).paddingAll(8), ), Positioned( left: 12, bottom: 8, child: Text( "${currentPage + 1}/${widget.urls.length}", ), ) ], ); }, ), ), ); } Widget buildActions() { var width = MediaQuery.of(context).size.width; return FlyoutTarget( controller: menuController, child: width > 600 ? Button( onPressed: showMenu, child: Row( children: [ const Icon( MdIcons.menu, size: 18, ), const SizedBox( width: 8, ), Text('Actions'.tl), ], )) : IconButton( icon: const Icon( MdIcons.more_horiz, size: 20, ), onPressed: showMenu), ); } ImageProvider getImageProvider(String url) { if (url.startsWith("file://")) { return FileImage(File(url.replaceFirst("file://", ""))); } else if (url.startsWith("novel:")) { var ids = url.split(':').last.split('/'); return CachedNovelImageProvider(ids[0], ids[1]); } return CachedImageProvider(url) as ImageProvider; } }