From cb356dbf718655c4aa65e3a35948b4e36119781d Mon Sep 17 00:00:00 2001 From: wgh19 Date: Fri, 31 May 2024 17:38:27 +0800 Subject: [PATCH] shortcuts --- assets/tr.json | 20 +++- lib/appdata.dart | 9 ++ lib/components/keyboard.dart | 61 ++++++++++++ lib/main.dart | 11 ++- lib/pages/illust_page.dart | 173 +++++++++++++++++++++++++---------- lib/pages/main_page.dart | 9 +- lib/pages/settings_page.dart | 88 ++++++++++++++++++ pubspec.lock | 56 ++++++++---- 8 files changed, 353 insertions(+), 74 deletions(-) create mode 100644 lib/components/keyboard.dart diff --git a/assets/tr.json b/assets/tr.json index 70ce16c..9aaf952 100644 --- a/assets/tr.json +++ b/assets/tr.json @@ -148,7 +148,15 @@ "Local": "本地", "Both": "同时", "This artwork is blocked": "此作品已被屏蔽", - "Delete Invalid Items": "删除无效项目" + "Delete Invalid Items": "删除无效项目", + "Private Favorite": "私人收藏", + "Shortcuts": "快捷键", + "Page down": "向下翻页", + "Page up": "向上翻页", + "Next work": "下一作品", + "Previous work": "上一作品", + "Add to favorites": "添加收藏", + "Follow the artist": "关注画师" }, "zh_TW": { "Search": "搜索", @@ -299,6 +307,14 @@ "Local": "本地", "Both": "同時", "This artwork is blocked": "此作品已被屏蔽", - "Delete Invalid Items": "刪除無效項目" + "Delete Invalid Items": "刪除無效項目", + "Private Favorite": "私人收藏", + "Shortcuts": "快捷鍵", + "Page down": "向下翻頁", + "Page up": "向上翻頁", + "Next work": "下一作品", + "Previous work": "上一作品", + "Add to favorites": "添加收藏", + "Follow the artist": "關注畫師" } } \ No newline at end of file diff --git a/lib/appdata.dart b/lib/appdata.dart index f49d574..da750df 100644 --- a/lib/appdata.dart +++ b/lib/appdata.dart @@ -24,6 +24,15 @@ class _Appdata { "readingLineHeight": 1.5, "readingParagraphSpacing": 8.0, "blockTags": [], + "shortcuts": [ + LogicalKeyboardKey.arrowDown.keyId, + LogicalKeyboardKey.arrowUp.keyId, + LogicalKeyboardKey.arrowRight.keyId, + LogicalKeyboardKey.arrowLeft.keyId, + LogicalKeyboardKey.enter.keyId, + LogicalKeyboardKey.keyD.keyId, + LogicalKeyboardKey.keyF.keyId, + ], }; bool lock = false; diff --git a/lib/components/keyboard.dart b/lib/components/keyboard.dart new file mode 100644 index 0000000..162981e --- /dev/null +++ b/lib/components/keyboard.dart @@ -0,0 +1,61 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:pixes/foundation/app.dart'; + +typedef KeyEventHandler = void Function(LogicalKeyboardKey key); + +class KeyEventListener extends StatefulWidget { + const KeyEventListener({required this.child, super.key}); + + final Widget child; + + static KeyEventListenerState? of(BuildContext context) { + return context.findAncestorStateOfType(); + } + + @override + State createState() => KeyEventListenerState(); +} + +class KeyEventListenerState extends State { + final focusNode = FocusNode(); + + final List _handlers = []; + + void addHandler(KeyEventHandler handler) { + _handlers.add(handler); + } + + void removeHandler(KeyEventHandler handler) { + _handlers.remove(handler); + } + + void removeAll() { + _handlers.clear(); + } + + @override + Widget build(BuildContext context) { + return Focus( + focusNode: focusNode, + autofocus: true, + onKeyEvent: (node, event) { + if (event is! KeyUpEvent) return KeyEventResult.ignored; + if (event.logicalKey == LogicalKeyboardKey.escape) { + if (App.rootNavigatorKey.currentState?.canPop() ?? false) { + App.rootNavigatorKey.currentState?.pop(); + } + if (App.mainNavigatorKey?.currentState?.canPop() ?? false) { + App.mainNavigatorKey?.currentState?.pop(); + } + return KeyEventResult.handled; + } + for (var handler in _handlers) { + handler(event.logicalKey); + } + return KeyEventResult.handled; + }, + child: widget.child, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index e5c8b0a..8727071 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,6 +6,7 @@ import "package:flutter/material.dart" as md; import "package:flutter/services.dart"; import "package:flutter_acrylic/flutter_acrylic.dart" as flutter_acrylic; import "package:pixes/appdata.dart"; +import "package:pixes/components/keyboard.dart"; import "package:pixes/components/md.dart"; import "package:pixes/components/message.dart"; import "package:pixes/foundation/app.dart"; @@ -88,7 +89,7 @@ class MyApp extends StatelessWidget { title: 'pixes', theme: FluentThemeData( brightness: brightness, - fontFamily: App.isWindows ? 'font' : null, + fontFamily: App.isWindows ? '微软雅黑' : null, accentColor: AccentColor.swatch({ 'darkest': darken(colorScheme.primary, 30), 'darker': darken(colorScheme.primary, 20), @@ -97,7 +98,11 @@ class MyApp extends StatelessWidget { 'light': lighten(colorScheme.primary, 10), 'lighter': lighten(colorScheme.primary, 20), 'lightest': lighten(colorScheme.primary, 30) - })), + }), + focusTheme: const FocusThemeData( + primaryBorder: BorderSide.none, + secondaryBorder: BorderSide.none, + )), home: const MainPage(), builder: (context, child) { ErrorWidget.builder = (details) { @@ -151,7 +156,7 @@ class MyApp extends StatelessWidget { } } - return widget; + return KeyEventListener(child: widget); }); }, ), diff --git a/lib/pages/illust_page.dart b/lib/pages/illust_page.dart index fe3ad4e..104d66e 100644 --- a/lib/pages/illust_page.dart +++ b/lib/pages/illust_page.dart @@ -7,6 +7,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; import 'package:pixes/appdata.dart'; import 'package:pixes/components/animated_image.dart'; +import 'package:pixes/components/keyboard.dart'; import 'package:pixes/components/loading.dart'; import 'package:pixes/components/message.dart'; import 'package:pixes/components/page_route.dart'; @@ -154,8 +155,15 @@ class IllustPage extends StatefulWidget { class _IllustPageState extends State { String get id => "${widget.illust.author.id}#${widget.illust.id}"; + final _bottomBarController = _BottomBarController(); + + KeyEventListenerState? keyboardListener; + @override void initState() { + keyboardListener = KeyEventListener.of(context); + keyboardListener?.removeAll(); + keyboardListener?.addHandler(handleKey); IllustPage.followCallbacks[id] = (v) { setState(() { widget.illust.author.isFollowed = v; @@ -166,6 +174,7 @@ class _IllustPageState extends State { @override void dispose() { + keyboardListener?.removeHandler(handleKey); IllustPage.followCallbacks.remove(id); super.dispose(); } @@ -173,7 +182,7 @@ class _IllustPageState extends State { @override Widget build(BuildContext context) { var isBlocked = checkIllusts([widget.illust]).isEmpty; - return buildKeyboardListener(ColoredBox( + return ColoredBox( color: FluentTheme.of(context).micaBackgroundColor, child: SizedBox.expand( child: ColoredBox( @@ -195,6 +204,7 @@ class _IllustPageState extends State { constrains.maxHeight, constrains.maxWidth, updateCallback: () => setState(() {}), + controller: _bottomBarController, ), if (isBlocked) const Positioned.fill( @@ -209,36 +219,53 @@ class _IllustPageState extends State { }), ), ), - )); + ); } final scrollController = ScrollController(); - Widget buildKeyboardListener(Widget child) { - return KeyboardListener( - focusNode: FocusNode(), - autofocus: true, - onKeyEvent: (event) { - if (event is! KeyUpEvent) return; - const kShortcutScrollOffset = 200; - if (event.logicalKey == LogicalKeyboardKey.arrowDown) { + void handleKey(LogicalKeyboardKey key) { + const kShortcutScrollOffset = 200; + + var shortcuts = appdata.settings["shortcuts"] as List; + + switch (shortcuts.indexOf(key.keyId)) { + case 0: + if (scrollController.position.pixels >= + scrollController.position.maxScrollExtent) { + _bottomBarController.openOrClose(); + } else { scrollController.animateTo( - scrollController.offset + kShortcutScrollOffset, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut); - } else if (event.logicalKey == LogicalKeyboardKey.arrowUp) { - scrollController.animateTo( - scrollController.offset - kShortcutScrollOffset, - duration: const Duration(milliseconds: 200), - curve: Curves.easeOut); - } else if (event.logicalKey == LogicalKeyboardKey.arrowRight) { - widget.nextPage?.call(); - } else if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { - widget.previousPage?.call(); + scrollController.offset + kShortcutScrollOffset, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); } - }, - child: child, - ); + break; + case 1: + if (_bottomBarController.isOpen()) { + _bottomBarController.openOrClose(); + break; + } + scrollController.animateTo( + scrollController.offset - kShortcutScrollOffset, + duration: const Duration(milliseconds: 200), + curve: Curves.easeOut, + ); + break; + case 2: + widget.nextPage?.call(); + break; + case 3: + widget.previousPage?.call(); + break; + case 4: + _bottomBarController.favorite(); + case 5: + _bottomBarController.download(); + case 6: + _bottomBarController.follow(); + } } Widget buildBody(double width, double height) { @@ -344,8 +371,31 @@ class _IllustPageState extends State { } } +class _BottomBarController { + VoidCallback? _openOrClose; + + VoidCallback get openOrClose => _openOrClose!; + + bool Function()? _isOpen; + + bool isOpen() => _isOpen!(); + + VoidCallback? _favorite; + + VoidCallback get favorite => _favorite!; + + VoidCallback? _download; + + VoidCallback get download => _download!; + + VoidCallback? _follow; + + VoidCallback get follow => _follow!; +} + class _BottomBar extends StatefulWidget { - const _BottomBar(this.illust, this.height, this.width, {this.updateCallback}); + const _BottomBar(this.illust, this.height, this.width, + {this.updateCallback, this.controller}); final void Function()? updateCallback; @@ -355,6 +405,8 @@ class _BottomBar extends StatefulWidget { final double width; + final _BottomBarController? controller; + @override State<_BottomBar> createState() => _BottomBarState(); } @@ -391,9 +443,32 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { ..onCancel = _handlePointerCancel; animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 180), value: 1); + if (widget.controller != null) { + widget.controller!._openOrClose = () { + if (animationController.value == 0) { + animationController.animateTo(1); + } else if (animationController.value == 1) { + animationController.animateTo(0); + } + }; + widget.controller!._isOpen = () => animationController.value == 0; + widget.controller!._favorite = favorite; + widget.controller!._download = () { + DownloadManager().addDownloadingTask(widget.illust); + setState(() {}); + }; + widget.controller!._follow = follow; + } super.initState(); } + @override + void dispose() { + animationController.dispose(); + _recognizer.dispose(); + super.dispose(); + } + void _handlePointerDown(DragStartDetails details) {} void _handlePointerMove(DragUpdateDetails details) { var offset = details.primaryDelta ?? 0; @@ -541,31 +616,31 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { bool isFollowing = false; - Widget buildAuthor() { - void follow() async { - if (isFollowing) return; - setState(() { - isFollowing = true; - }); - var method = widget.illust.author.isFollowed ? "delete" : "add"; - var res = - await Network().follow(widget.illust.author.id.toString(), method); - if (res.error) { - if (mounted) { - context.showToast(message: "Network Error"); - } - } else { - widget.illust.author.isFollowed = !widget.illust.author.isFollowed; + void follow() async { + if (isFollowing) return; + setState(() { + isFollowing = true; + }); + var method = widget.illust.author.isFollowed ? "delete" : "add"; + var res = + await Network().follow(widget.illust.author.id.toString(), method); + if (res.error) { + if (mounted) { + context.showToast(message: "Network Error"); } - setState(() { - isFollowing = false; - }); - UserInfoPage.followCallbacks[widget.illust.author.id.toString()] - ?.call(widget.illust.author.isFollowed); - UserPreviewWidget.followCallbacks[widget.illust.author.id.toString()] - ?.call(widget.illust.author.isFollowed); + } else { + widget.illust.author.isFollowed = !widget.illust.author.isFollowed; } + setState(() { + isFollowing = false; + }); + UserInfoPage.followCallbacks[widget.illust.author.id.toString()] + ?.call(widget.illust.author.isFollowed); + UserPreviewWidget.followCallbacks[widget.illust.author.id.toString()] + ?.call(widget.illust.author.isFollowed); + } + Widget buildAuthor() { final bool showUserName = MediaQuery.of(context).size.width > 640; return Card( @@ -981,7 +1056,7 @@ class _BottomBarState extends State<_BottomBar> with TickerProviderStateMixin { ).fixWidth(96), Button( onPressed: () { - var text = "https://www.pixiv.net/artworks/${widget.illust.id}"; + var text = "https://pixiv.net/artworks/${widget.illust.id}"; Clipboard.setData(ClipboardData(text: text)); showToast(context, message: "Copied".tl); }, diff --git a/lib/pages/main_page.dart b/lib/pages/main_page.dart index c9cbe4e..721247f 100644 --- a/lib/pages/main_page.dart +++ b/lib/pages/main_page.dart @@ -3,6 +3,7 @@ import "dart:async"; import "package:fluent_ui/fluent_ui.dart"; import "package:flutter/foundation.dart"; import "package:pixes/appdata.dart"; +import "package:pixes/components/keyboard.dart"; import "package:pixes/components/md.dart"; import "package:pixes/foundation/app.dart"; import "package:pixes/foundation/image_provider.dart"; @@ -214,10 +215,10 @@ class _MainPageState extends State with WindowListener { context: context, removeTop: true, child: Navigator( - key: navigatorKey, - onGenerateRoute: (settings) => AppPageRoute( - builder: (context) => const RecommendationPage()), - ), + key: navigatorKey, + onGenerateRoute: (settings) => AppPageRoute( + builder: (context) => const RecommendationPage()), + ), ))), ); } diff --git a/lib/pages/settings_page.dart b/lib/pages/settings_page.dart index f200da2..79f2a9d 100644 --- a/lib/pages/settings_page.dart +++ b/lib/pages/settings_page.dart @@ -1,7 +1,9 @@ import 'dart:io'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; import 'package:pixes/appdata.dart'; +import 'package:pixes/components/keyboard.dart'; import 'package:pixes/components/md.dart'; import 'package:pixes/components/message.dart'; import 'package:pixes/components/page_route.dart'; @@ -242,6 +244,14 @@ class _SettingsPageState extends State { context.to(() => const _BlockTagsPage()); }, )), + buildItem( + title: "Shortcuts".tl, + action: Button( + child: Text("Edit".tl).fixWidth(64), + onPressed: () { + context.to(() => const ShortcutsSettings()); + }, + )), ], ), ); @@ -538,3 +548,81 @@ class __BlockTagsPageState extends State<_BlockTagsPage> { ); } } + +class ShortcutsSettings extends StatefulWidget { + const ShortcutsSettings({super.key}); + + @override + State createState() => _ShortcutsSettingsState(); +} + +class _ShortcutsSettingsState extends State { + int listening = -1; + + KeyEventListenerState? listener; + + @override + void initState() { + listener = KeyEventListener.of(context); + super.initState(); + } + + @override + void dispose() { + listener?.removeAll(); + super.dispose(); + } + + final settings = [ + "Page down", + "Page up", + "Next work", + "Previous work", + "Add to favorites", + "Download", + "Follow the artist", + ]; + + @override + Widget build(BuildContext context) { + return SingleChildScrollView( + child: Column(children: [ + TitleBar(title: "Shortcuts".tl), + ...settings.map((e) => buildItem(e, settings.indexOf(e))) + ]), + ); + } + + Widget buildItem(String text, int index) { + var keyText = listening == index + ? "Waiting..." + : LogicalKeyboardKey(appdata.settings['shortcuts'][index]).keyLabel; + return Card( + padding: EdgeInsets.zero, + margin: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), + child: ListTile( + title: Text(text.tl), + trailing: Button( + child: Text(keyText), + onPressed: () { + if (listening != -1) { + listener?.removeAll(); + } + setState(() { + listening = index; + }); + listener?.addHandler((key) { + if (key == LogicalKeyboardKey.escape) return; + setState(() { + appdata.settings['shortcuts'][index] = key.keyId; + listening = -1; + appdata.writeData(); + }); + Future.microtask(() => listener?.removeAll()); + }); + }, + ), + ), + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index ce46f81..3010146 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -73,6 +73,22 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.3" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + url: "https://pub.dev" + source: hosted + version: "10.1.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" + source: hosted + version: "7.0.0" dio: dependency: "direct main" description: @@ -81,6 +97,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.3+1" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" fake_async: dependency: transitive description: @@ -190,6 +214,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_acrylic: + dependency: "direct main" + description: + name: flutter_acrylic + sha256: a9a1fdf91ff1fb47858fd82507f57e255a132a5d355056694fdb9fd303633b18 + url: "https://pub.dev" + source: hosted + version: "1.1.3" flutter_file_dialog: dependency: "direct main" description: @@ -293,6 +325,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + macos_window_utils: + dependency: transitive + description: + name: macos_window_utils + sha256: "230be594d26f6dee92c5a1544f4242d25138a5bfb9f185b27f14de3949ef0be8" + url: "https://pub.dev" + source: hosted + version: "1.5.0" matcher: dependency: transitive description: @@ -515,22 +555,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" - system_theme: - dependency: "direct main" - description: - name: system_theme - sha256: "1f208db140a3d1e1eac2034b54920d95699c1534df576ced44b3312c5de3975f" - url: "https://pub.dev" - source: hosted - version: "2.3.1" - system_theme_web: - dependency: transitive - description: - name: system_theme_web - sha256: "7566f5a928f6d28d7a60c97bea8a851d1c6bc9b86a4df2366230a97458489219" - url: "https://pub.dev" - source: hosted - version: "0.0.2" term_glyph: dependency: transitive description: