From faa857b814aae7d90e0fe4294879038d9a6a2e04 Mon Sep 17 00:00:00 2001 From: wgh19 Date: Tue, 14 May 2024 21:48:33 +0800 Subject: [PATCH] add actions --- lib/components/message.dart | 2 +- lib/pages/downloaded_page.dart | 77 +++++++++++++++++- lib/pages/illust_page.dart | 143 ++++++++++++++++++++++++++------ lib/pages/image_page.dart | 111 +++++++++++++++++++++---- lib/pages/user_info_page.dart | 26 +++++- lib/utils/io.dart | 25 ++++++ macos/Runner/Info.plist | 2 + pubspec.lock | 144 +++++++++++++++++++++++++++++++++ pubspec.yaml | 3 + 9 files changed, 490 insertions(+), 43 deletions(-) diff --git a/lib/components/message.dart b/lib/components/message.dart index f642c2d..3f7fb3d 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -28,7 +28,7 @@ class ToastOverlay extends StatelessWidget { child: Align( alignment: Alignment.bottomCenter, child: PhysicalModel( - color: ColorScheme.of(context).surface, + color: ColorScheme.of(context).surface.withOpacity(1), borderRadius: BorderRadius.circular(4), elevation: 1, child: Container( diff --git a/lib/pages/downloaded_page.dart b/lib/pages/downloaded_page.dart index deba521..d5fea2c 100644 --- a/lib/pages/downloaded_page.dart +++ b/lib/pages/downloaded_page.dart @@ -10,8 +10,10 @@ import 'package:pixes/components/title_bar.dart'; import 'package:pixes/foundation/app.dart'; import 'package:pixes/network/download.dart'; import 'package:pixes/utils/translation.dart'; +import 'package:share_plus/share_plus.dart'; import 'package:window_manager/window_manager.dart'; +import '../utils/io.dart'; import 'main_page.dart'; class DownloadedPage extends StatefulWidget { @@ -208,6 +210,46 @@ class _DownloadedIllustViewPageState extends State<_DownloadedIllustViewPage> wi var controller = PageController(); + int currentPage = 0; + + var menuController = FlyoutController(); + + Future getFile() async { + var file = File(widget.imagePaths[currentPage]); + if(file.existsSync()) { + return file; + } + return null; + } + + void showMenu() { + menuController.showFlyout(builder: (context) => MenuFlyout( + items: [ + MenuFlyoutItem(text: Text("Save to".tl), onPressed: () async{ + var file = await getFile(); + if(file != null){ + saveFile(file); + } + }), + MenuFlyoutItem(text: Text("Share".tl), onPressed: () async{ + var file = await getFile(); + if(file != null){ + var ext = file.path.split('.').last; + 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(file.path, mimeType: mediaType, name: file.path.split('/').last)]); + } + }), + ], + )); + } + @override Widget build(BuildContext context) { return ScaffoldPage( @@ -240,7 +282,9 @@ class _DownloadedIllustViewPageState extends State<_DownloadedIllustViewPage> wi ); }, onPageChanged: (index) { - setState(() {}); + setState(() { + currentPage = index; + }); }, )), Positioned( @@ -259,6 +303,7 @@ class _DownloadedIllustViewPageState extends State<_DownloadedIllustViewPage> wi const Expanded( child: DragToMoveArea(child: SizedBox.expand(),), ), + buildActions(), if(App.isDesktop) WindowButtons(key: ValueKey(windowButtonKey),), ], @@ -295,7 +340,7 @@ class _DownloadedIllustViewPageState extends State<_DownloadedIllustViewPage> wi left: 12, bottom: 8, child: Text( - "${controller.page!.toInt() + 1}/${widget.imagePaths.length}", + "${currentPage + 1}/${widget.imagePaths.length}", ), ) ], @@ -305,5 +350,33 @@ class _DownloadedIllustViewPageState extends State<_DownloadedIllustViewPage> wi ), ); } + + Widget buildActions() { + var width = MediaQuery.of(context).size.width; + return FlyoutTarget( + controller: menuController, + child: width > 600 + ? Button( + onPressed: showMenu, + child: const Row( + children: [ + Icon( + MdIcons.menu, + size: 18, + ), + SizedBox( + width: 8, + ), + Text('Actions'), + ], + )) + : IconButton( + icon: const Icon( + MdIcons.more_horiz, + size: 20, + ), + onPressed: showMenu), + ); + } } diff --git a/lib/pages/illust_page.dart b/lib/pages/illust_page.dart index 3bbb135..fa82b63 100644 --- a/lib/pages/illust_page.dart +++ b/lib/pages/illust_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' show Icons; +import 'package:flutter/services.dart'; import 'package:pixes/components/animated_image.dart'; import 'package:pixes/components/loading.dart'; import 'package:pixes/components/message.dart'; @@ -15,6 +16,7 @@ import 'package:pixes/pages/image_page.dart'; import 'package:pixes/pages/search_page.dart'; import 'package:pixes/pages/user_info_page.dart'; import 'package:pixes/utils/translation.dart'; +import 'package:share_plus/share_plus.dart'; import '../components/md.dart'; @@ -203,18 +205,21 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{ top = (top + offset).clamp(minValue, maxValue); animationController.value = (top - minValue) / (maxValue - minValue); } + void _handlePointerUp(DragEndDetails details) { var speed = details.primaryVelocity ?? 0; const minShouldTransitionSpeed = 1000; if(speed > minShouldTransitionSpeed) { animationController.forward(); - } else if(speed < minShouldTransitionSpeed) { + } else if(speed < 0 - minShouldTransitionSpeed) { animationController.reverse(); } else { _handlePointerCancel(); } } + void _handlePointerCancel() { + if(animationController.value == 1 || animationController.value == 0) return; if(animationController.value >= 0.5 ) { animationController.forward(); } else { @@ -261,6 +266,16 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{ onPointerDown: (event) { _recognizer.addPointer(event); }, + onPointerSignal: (event) { + if(event is PointerScrollEvent) { + var offset = (event).scrollDelta.dy; + if(offset < 0) { + animationController.reverse(); + } else { + animationController.forward(); + } + } + }, child: Card( borderRadius: const BorderRadius.vertical(top: Radius.circular(8)), backgroundColor: @@ -275,6 +290,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{ buildTop(), buildStats(), buildTags(), + buildMoreActions(), SelectableText("${"Artwork ID".tl}: ${widget.illust.id}\n${"Artist ID".tl}: ${widget.illust.author.id}", style: TextStyle(color: ColorScheme.of(context).outline),).paddingLeft(4), const SizedBox(height: 8,) ], @@ -395,14 +411,14 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{ ), )) else if (!widget.illust.author.isFollowed) - Button(onPressed: follow, child: Text("Follow".tl).fixWidth(56)) + Button(onPressed: follow, child: Text("Follow".tl).fixWidth(62)) else Button( onPressed: follow, child: Text( "Unfollow".tl, style: TextStyle(color: ColorScheme.of(context).error), - ).fixWidth(56), + ).fixWidth(62), ), ], ), @@ -412,29 +428,29 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{ bool isBookmarking = false; + void favorite([String type = "public"]) async{ + if(isBookmarking) return; + setState(() { + isBookmarking = true; + }); + var method = widget.illust.isBookmarked ? "delete" : "add"; + var res = await Network().addBookmark(widget.illust.id.toString(), method, type); + if(res.error) { + if(mounted) { + context.showToast(message: "Network Error"); + } + } else { + widget.illust.isBookmarked = !widget.illust.isBookmarked; + widget.favoriteCallback?.call(widget.illust.isBookmarked); + } + setState(() { + isBookmarking = false; + }); + } + Iterable buildActions(double width) sync* { yield const SizedBox(width: 8,); - void favorite() async{ - if(isBookmarking) return; - setState(() { - isBookmarking = true; - }); - var method = widget.illust.isBookmarked ? "delete" : "add"; - var res = await Network().addBookmark(widget.illust.id.toString(), method); - if(res.error) { - if(mounted) { - context.showToast(message: "Network Error"); - } - } else { - widget.illust.isBookmarked = !widget.illust.isBookmarked; - widget.favoriteCallback?.call(widget.illust.isBookmarked); - } - setState(() { - isBookmarking = false; - }); - } - void download() { DownloadManager().addDownloadingTask(widget.illust); setState(() {}); @@ -629,6 +645,87 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin{ ), ).paddingVertical(8).paddingHorizontal(2); } + + Widget buildMoreActions() { + return Row( + children: [ + Button( + onPressed: () => favorite("private"), + child: SizedBox( + height: 28, + child: Row( + children: [ + if(isBookmarking) + const SizedBox( + width: 18, + height: 18, + child: ProgressRing(strokeWidth: 2,), + ) + else if(widget.illust.isBookmarked) + Icon( + Icons.favorite, + color: ColorScheme.of(context).error, + size: 18, + ) + else + const Icon( + Icons.favorite_border, + size: 18, + ), + const SizedBox(width: 8,), + if(widget.illust.isBookmarked) + Text("Cancel".tl) + else + Text("Private".tl) + ], + ), + ), + ), + const SizedBox(width: 8,), + Button( + onPressed: () { + Share.share("${widget.illust.title}\nhttps://pixiv.net/artworks/${widget.illust.id}"); + }, + child: SizedBox( + height: 28, + child: Row( + children: [ + Icon( + Icons.share, + color: ColorScheme.of(context).error, + size: 18, + ), + const SizedBox(width: 8,), + Text("Share".tl) + ], + ), + ), + ), + const SizedBox(width: 8,), + Button( + onPressed: () { + var text = "https://pixiv.net/artworks/${widget.illust.id}"; + Clipboard.setData(ClipboardData(text: text)); + showToast(context, message: "Copied".tl); + }, + child: SizedBox( + height: 28, + child: Row( + children: [ + Icon( + Icons.copy, + color: ColorScheme.of(context).error, + size: 18, + ), + const SizedBox(width: 8,), + Text("Link".tl) + ], + ), + ), + ), + ], + ).paddingHorizontal(4).paddingBottom(4); + } } class _CommentsPage extends StatefulWidget { diff --git a/lib/pages/image_page.dart b/lib/pages/image_page.dart index 71aea2f..55d781e 100644 --- a/lib/pages/image_page.dart +++ b/lib/pages/image_page.dart @@ -2,10 +2,15 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; import 'package:photo_view/photo_view.dart'; +import 'package:pixes/components/md.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 { @@ -14,15 +19,15 @@ class ImagePage extends StatefulWidget { final String url; static show(String url) { - App.rootNavigatorKey.currentState?.push( - AppPageRoute(builder: (context) => ImagePage(url))); + App.rootNavigatorKey.currentState + ?.push(AppPageRoute(builder: (context) => ImagePage(url))); } @override State createState() => _ImagePageState(); } -class _ImagePageState extends State with WindowListener{ +class _ImagePageState extends State with WindowListener { int windowButtonKey = 0; @override @@ -53,14 +58,15 @@ class _ImagePageState extends State with WindowListener{ @override Widget build(BuildContext context) { - return ScaffoldPage( + return Container( padding: EdgeInsets.only(top: MediaQuery.of(context).padding.top), - content: Stack( + color: FluentTheme.of(context).micaBackgroundColor, + child: Stack( children: [ - Positioned.fill(child: PhotoView( - backgroundDecoration: const BoxDecoration( - color: Colors.transparent - ), + Positioned.fill( + child: PhotoView( + backgroundDecoration: BoxDecoration( + color: FluentTheme.of(context).micaBackgroundColor), filterQuality: FilterQuality.medium, imageProvider: widget.url.startsWith("file://") ? FileImage(File(widget.url.replaceFirst("file://", ""))) @@ -74,16 +80,22 @@ class _ImagePageState extends State with WindowListener{ height: 36, child: Row( children: [ - const SizedBox(width: 6,), + const SizedBox( + width: 6, + ), IconButton( icon: const Icon(FluentIcons.back).paddingAll(2), - onPressed: () => context.pop() - ), + onPressed: () => context.pop()), const Expanded( - child: DragToMoveArea(child: SizedBox.expand(),), + child: DragToMoveArea( + child: SizedBox.expand(), + ), ), - if(App.isDesktop) - WindowButtons(key: ValueKey(windowButtonKey),), + buildActions(), + if (App.isDesktop) + WindowButtons( + key: ValueKey(windowButtonKey), + ), ], ), ), @@ -92,4 +104,73 @@ class _ImagePageState extends State with WindowListener{ ), ); } + + var menuController = FlyoutController(); + + Future getFile() async{ + if (widget.url.startsWith("file://")) { + return File(widget.url.replaceFirst("file://", "")); + } + var res = await CacheManager().findCache(widget.url); + if(res == null){ + return null; + } + return File(res); + } + + void showMenu() { + menuController.showFlyout(builder: (context) => MenuFlyout( + items: [ + MenuFlyoutItem(text: Text("Save to".tl), onPressed: () async{ + var file = await getFile(); + if(file != null){ + saveFile(file); + } + }), + MenuFlyoutItem(text: Text("Share".tl), onPressed: () async{ + var file = await getFile(); + if(file != null){ + var ext = file.path.split('.').last; + 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(file.path, mimeType: mediaType, name: file.path.split('/').last)]); + } + }), + ], + )); + } + + Widget buildActions() { + var width = MediaQuery.of(context).size.width; + return FlyoutTarget( + controller: menuController, + child: width > 600 + ? Button( + onPressed: showMenu, + child: const Row( + children: [ + Icon( + MdIcons.menu, + size: 18, + ), + SizedBox( + width: 8, + ), + Text('Actions'), + ], + )) + : IconButton( + icon: const Icon( + MdIcons.more_horiz, + size: 20, + ), + onPressed: showMenu), + ); + } } diff --git a/lib/pages/user_info_page.dart b/lib/pages/user_info_page.dart index 90b4577..d4c5237 100644 --- a/lib/pages/user_info_page.dart +++ b/lib/pages/user_info_page.dart @@ -43,11 +43,28 @@ class _UserInfoPageState extends LoadingState { void follow() async{ if(isFollowing) return; + String type = ""; + if(!data!.isFollowed) { + await flyoutController.showFlyout( + navigatorKey: App.rootNavigatorKey.currentState, + builder: (context) => + MenuFlyout( + items: [ + MenuFlyoutItem(text: Text("Public".tl), + onPressed: () => type = "public"), + MenuFlyoutItem(text: Text("Private".tl), + onPressed: () => type = "private"), + ], + )); + } + if(type.isEmpty && !data!.isFollowed) { + return; + } setState(() { isFollowing = true; }); var method = data!.isFollowed ? "delete" : "add"; - var res = await Network().follow(data!.id.toString(), method); + var res = await Network().follow(data!.id.toString(), method, type); if(res.error) { if(mounted) { context.showToast(message: "Network Error"); @@ -61,6 +78,8 @@ class _UserInfoPageState extends LoadingState { }); } + var flyoutController = FlyoutController(); + Widget buildUser() { return SliverToBoxAdapter( child: Column( @@ -112,7 +131,10 @@ class _UserInfoPageState extends LoadingState { ), )) else if (!data!.isFollowed) - Button(onPressed: follow, child: Text("Follow".tl)) + FlyoutTarget( + controller: flyoutController, + child: Button(onPressed: follow, child: Text("Follow".tl)) + ) else Button( onPressed: follow, diff --git a/lib/utils/io.dart b/lib/utils/io.dart index 89e747c..5085131 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -1,4 +1,9 @@ import 'dart:io'; +import 'dart:typed_data'; + +import 'package:file_selector/file_selector.dart'; +import 'package:flutter_file_dialog/flutter_file_dialog.dart'; +import 'package:pixes/foundation/app.dart'; extension FSExt on FileSystemEntity { Future deleteIfExists() async { @@ -43,4 +48,24 @@ String bytesToText(int bytes) { } else { return "${(bytes / 1024 / 1024 / 1024).toStringAsFixed(2)} GB"; } +} + +void saveFile(File file) async{ + if(App.isDesktop) { + var fileName = file.path.split('/').last; + final FileSaveLocation? result = + await getSaveLocation(suggestedName: fileName); + if (result == null) { + return; + } + + final Uint8List fileData = await file.readAsBytes(); + String mimeType = 'image/${fileName.split('.').last}'; + final XFile textFile = XFile.fromData( + fileData, mimeType: mimeType, name: fileName); + await textFile.saveTo(result.path); + } else { + final params = SaveFileDialogParams(sourceFilePath: file.path); + await FlutterFileDialog.saveFile(params: params); + } } \ No newline at end of file diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist index 2afed4d..293ddde 100644 --- a/macos/Runner/Info.plist +++ b/macos/Runner/Info.plist @@ -40,5 +40,7 @@ + com.apple.security.files.user-selected.read-write + diff --git a/pubspec.lock b/pubspec.lock index c19992b..df01ba6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,6 +49,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + url: "https://pub.dev" + source: hosted + version: "0.3.4+1" crypto: dependency: "direct main" description: @@ -81,6 +89,86 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + file: + dependency: transitive + description: + name: file + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + file_selector: + dependency: "direct main" + description: + name: file_selector + sha256: "5019692b593455127794d5718304ff1ae15447dea286cdda9f0db2a796a1b828" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + file_selector_android: + dependency: transitive + description: + name: file_selector_android + sha256: "57265ec9591e8fd8508f613544cde6f7d045731f6b09644057e49a4c9c672b7c" + url: "https://pub.dev" + source: hosted + version: "0.5.1+1" + file_selector_ios: + dependency: transitive + description: + name: file_selector_ios + sha256: "7160121e434910ec23717bde3a0c514ca039e8c97b791ff35d1786da38abcb4a" + url: "https://pub.dev" + source: hosted + version: "0.5.2" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" + url: "https://pub.dev" + source: hosted + version: "0.9.2+1" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 + url: "https://pub.dev" + source: hosted + version: "0.9.4" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_web: + dependency: transitive + description: + name: file_selector_web + sha256: "619e431b224711a3869e30dbd7d516f5f5a4f04b265013a50912f39e1abc88c8" + url: "https://pub.dev" + source: hosted + version: "0.9.4+1" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0 + url: "https://pub.dev" + source: hosted + version: "0.9.3+1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" fluent_ui: dependency: "direct main" description: @@ -94,6 +182,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_file_dialog: + dependency: "direct main" + description: + name: flutter_file_dialog + sha256: "5a1507833473b38839056d63c5125750a6d12e904f78131324fa4632504de513" + url: "https://pub.dev" + source: hosted + version: "3.0.1" flutter_lints: dependency: "direct dev" description: @@ -133,6 +229,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + http: + dependency: transitive + description: + name: http + sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + url: "https://pub.dev" + source: hosted + version: "1.2.1" http_parser: dependency: transitive description: @@ -213,6 +317,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.11.0" + mime: + dependency: transitive + description: + name: mime + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" + url: "https://pub.dev" + source: hosted + version: "1.0.5" path: dependency: transitive description: @@ -318,6 +430,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + share_plus: + dependency: "direct main" + description: + name: share_plus + sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 + url: "https://pub.dev" + source: hosted + version: "9.0.0" + share_plus_platform_interface: + dependency: transitive + description: + name: share_plus_platform_interface + sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" + url: "https://pub.dev" + source: hosted + version: "4.0.0" sky_engine: dependency: transitive description: flutter @@ -331,6 +459,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqlite3: dependency: "direct main" description: @@ -475,6 +611,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + uuid: + dependency: transitive + description: + name: uuid + sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + url: "https://pub.dev" + source: hosted + version: "4.4.0" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 676d32f..621e91f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,6 +51,9 @@ dependencies: git: url: https://github.com/wgh136/photo_view ref: main + share_plus: ^9.0.0 + file_selector: ^1.0.1 + flutter_file_dialog: 3.0.1 dev_dependencies: flutter_test: sdk: flutter