From 2b7b13da82c49b3ee5c8f7864092434692c4afcd Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sat, 20 Jan 2024 16:46:06 +0800 Subject: [PATCH 01/24] feat(mobile):more options in setting fix:full screen in comic and novel reader feat(indicator bar): battery indicator, feat:wakelock --- lib/controllers/watch/comic_controller.dart | 38 ++++- lib/controllers/watch/novel_controller.dart | 7 +- lib/utils/miru_storage.dart | 3 + .../reader/comic/comic_reader_content.dart | 72 +++++++--- .../reader/comic/comic_reader_settings.dart | 130 +++++++++++++----- .../reader/novel/novel_reader_settings.dart | 47 ++++--- .../widgets/watch/control_panel_header.dart | 8 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 34 ++++- pubspec.yaml | 3 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 269 insertions(+), 79 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index fb6bfc41..aa9d360c 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -1,4 +1,5 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:miru_app/data/providers/anilist_provider.dart'; @@ -8,6 +9,10 @@ import 'package:miru_app/data/services/database_service.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; import 'package:miru_app/utils/miru_storage.dart'; +import 'dart:async'; +import 'package:battery_plus/battery_plus.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:miru_app/utils/i18n.dart'; class ComicController extends ReaderController { ComicController({ @@ -38,17 +43,39 @@ class ComicController extends ReaderController { final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); final scrollOffsetController = ScrollOffsetController(); - + final alignMode = Alignment.bottomLeft.obs; // 是否已经恢复上次阅读 final isRecover = false.obs; - + final batteryLevel = 100.obs; // 是否按下 ctrl - + Timer? _barreryTimer; + final statusBarElement = { + 'reader-setting.battery'.i18n: true.obs, + 'reader-setting.time'.i18n: true.obs, + 'reader-setting.page-indicator'.i18n: true.obs, + 'reader-setting.battery-icon'.i18n: true.obs, + }; final isZoom = false.obs; + final currentTime = "".obs; + Future _statusBar([Timer? t]) async { + final battery = Battery(); + batteryLevel.value = await battery.batteryLevel; + final datenow = DateTime.now(); + final hour = datenow.hour < 10 ? "0${datenow.hour}" : datenow.hour; + final minute = datenow.minute < 10 ? "0${datenow.minute}" : datenow.minute; + currentTime.value = "$hour:$minute"; + debugPrint("${datenow.toLocal()}"); + } @override - void onInit() { + void onInit() async { _initSetting(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + WakelockPlus.toggle( + enable: MiruStorage.getSetting(SettingKey.enableWakelock)); + await _statusBar(); + _barreryTimer = Timer.periodic( + const Duration(seconds: 10), (timer) => _statusBar(timer)); itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; @@ -196,6 +223,9 @@ class ComicController extends ReaderController { mediaId: anilistID, ); } + _barreryTimer!.cancel(); + WakelockPlus.disable(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.onClose(); } } diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index 27c760ef..cb0e4b69 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -4,6 +4,8 @@ import 'package:miru_app/controllers/watch/reader_controller.dart'; import 'package:miru_app/data/services/database_service.dart'; import 'package:miru_app/utils/miru_storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:flutter/services.dart'; class NovelController extends ReaderController { NovelController({ @@ -26,8 +28,10 @@ class NovelController extends ReaderController { @override void onInit() { super.onInit(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); fontSize.value = MiruStorage.getSetting(SettingKey.novelFontSize); - + WakelockPlus.toggle( + enable: MiruStorage.getSetting(SettingKey.enableWakelock)); itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; @@ -72,6 +76,7 @@ class NovelController extends ReaderController { totalProgress, ); } + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.onClose(); } } diff --git a/lib/utils/miru_storage.dart b/lib/utils/miru_storage.dart index 3a8540d9..e4f80242 100644 --- a/lib/utils/miru_storage.dart +++ b/lib/utils/miru_storage.dart @@ -129,6 +129,7 @@ class MiruStorage { await _initSetting(SettingKey.proxy, ''); await _initSetting(SettingKey.proxyType, 'DIRECT'); await _initSetting(SettingKey.saveLog, true); + await _initSetting(SettingKey.enableWakelock, false); } static _initSetting(String key, dynamic value) async { @@ -176,7 +177,9 @@ class SettingKey { static String keyJ = 'KeyJ'; static String arrowLeft = 'Arrowleft'; static String arrowRight = 'Arrowright'; + //reading mode static String readingMode = 'ReadingMode'; + static String enableWakelock = 'EnableWakelock'; static String aniListToken = 'AniListToken'; static String aniListUserId = 'AniListUserId'; static String autoTracking = 'AutoTracking'; diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index e45523b5..56f5d3b3 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; @@ -12,6 +11,7 @@ import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/progress.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; +import 'package:based_battery_indicator/based_battery_indicator.dart'; class ComicReaderContent extends StatefulWidget { const ComicReaderContent(this.tag, {super.key}); @@ -22,11 +22,6 @@ class ComicReaderContent extends StatefulWidget { } class _ComicReaderContentState extends State { - @override - void initState() { - super.initState(); - } - late final _c = Get.find(tag: widget.tag); // 按下数量 @@ -46,27 +41,66 @@ class _ComicReaderContentState extends State { ); } - _buildDisplay(Widget child) { + Widget _buildDisplay(Widget child) { + if (_c.statusBarElement.values.every((element) => element.value == false)) { + return child; + } return Stack( children: [ child, - Positioned( - bottom: 0, - child: Container( - color: Colors.black.withAlpha(200), - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 2), - child: Obx( - () => Text( - "${_c.currentPage.value + 1}/${_c.watchData.value?.urls.length ?? 0}", - style: const TextStyle(color: Colors.white, fontSize: 15), + Obx(() => Align( + alignment: _c.alignMode.value, + child: Container( + color: Colors.black.withAlpha(200), + padding: const EdgeInsets.fromLTRB(20, 2, 12, 2), + child: _indicatorBuilder(), ), - ), - ), - ), + )), ], ); } + Widget _indicatorBuilder() { + return Obx(() => Row(mainAxisSize: MainAxisSize.min, children: [ + if (_c.statusBarElement["reader-setting.page-indicator".i18n]! + .value) ...[ + Text( + "${_c.currentPage.value + 1}/${_c.watchData.value?.urls.length ?? 0}", + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + const SizedBox(width: 8) + ], + if (_c + .statusBarElement["reader-setting.battery-icon".i18n]!.value) ...[ + BasedBatteryIndicator( + status: BasedBatteryStatus( + value: _c.batteryLevel.value, + type: BasedBatteryStatusType.normal, + ), + trackHeight: 10.0, + trackAspectRatio: 2.0, + curve: Curves.ease, + duration: const Duration(seconds: 10), + ), + const SizedBox(width: 8) + ], + if (_c.statusBarElement["reader-setting.battery".i18n]!.value) ...[ + Text( + "${_c.batteryLevel.value}%", + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + const SizedBox(width: 8) + ], + if (_c.statusBarElement["reader-setting.time".i18n]!.value) ...[ + Text( + _c.currentTime.value, + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + const SizedBox(width: 8) + ], + ])); + } + _buildContent() { late Color backgroundColor; if (Platform.isAndroid) { diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index 7e494f49..ba7f7e03 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -4,7 +4,10 @@ import 'package:get/get.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/comic_controller.dart'; import 'package:miru_app/utils/i18n.dart'; +import 'package:miru_app/utils/miru_storage.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; +import 'package:miru_app/views/widgets/settings/settings_switch_tile.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; class ComicReaderSettings extends StatefulWidget { const ComicReaderSettings(this.tag, {super.key}); @@ -20,43 +23,100 @@ class _ComicReaderSettingsState extends State { Widget _buildAndroid(BuildContext context) { return Padding( padding: const EdgeInsets.all(16), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 阅读模式 - Text('comic-settings.read-mode'.i18n), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: SegmentedButton( - segments: [ - ButtonSegment( - value: MangaReadMode.standard, - label: Text('comic-settings.standard'.i18n), - ), - ButtonSegment( - value: MangaReadMode.rightToLeft, - label: Text('comic-settings.right-to-left'.i18n), + child: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 阅读模式 + + Text('comic-settings.read-mode'.i18n), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: [ + ButtonSegment( + value: MangaReadMode.standard, + label: Text('comic-settings.standard'.i18n), + ), + ButtonSegment( + value: MangaReadMode.rightToLeft, + label: Text('comic-settings.right-to-left'.i18n), + ), + ButtonSegment( + value: MangaReadMode.webTonn, + label: Text('comic-settings.web-tonn'.i18n), + ), + ], + selected: {_c.readType.value}, + onSelectionChanged: (value) { + if (value.isNotEmpty) { + _c.readType.value = value.first; + } + }, + showSelectedIcon: false, ), - ButtonSegment( - value: MangaReadMode.webTonn, - label: Text('comic-settings.web-tonn'.i18n), + ), + + const SizedBox(height: 16), + Text('comic-settings.indicator-alignment'.i18n), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: [ + ButtonSegment( + value: Alignment.bottomLeft, + label: Text('comic-settings.bottomLeft'.i18n), + ), + ButtonSegment( + value: Alignment.bottomRight, + label: Text('comic-settings.rightLeft'.i18n), + ), + ButtonSegment( + value: Alignment.topLeft, + label: Text('comic-settings.topLeft'.i18n), + ), + ButtonSegment( + value: Alignment.topRight, + label: Text('comic-settings.topRight'.i18n), + ) + ], + selected: {_c.alignMode.value}, + onSelectionChanged: (value) { + if (value.isNotEmpty) { + _c.alignMode.value = value.first; + } + }, + showSelectedIcon: false, ), - ], - selected: {_c.readType.value}, - onSelectionChanged: (value) { - if (value.isNotEmpty) { - setState(() { - _c.readType.value = value.first; - }); - } - }, - showSelectedIcon: false, - ), - ), - ], - ), + ), + const SizedBox(height: 16), + Text('comic-settings.status-bar'.i18n), + const SizedBox(height: 5), + Wrap( + spacing: 5, + children: _c.statusBarElement.keys + .map((e) => FilterChip( + label: Text(e), + selected: _c.statusBarElement[e]!.value, + onSelected: (val) { + _c.statusBarElement[e]!.value = val; + })) + .toList(), + ), + SettingsSwitchTile( + icon: const Icon(Icons.coffee), + title: "reader-settings.enable-wakelock".i18n, + buildValue: () => + MiruStorage.getSetting(SettingKey.enableWakelock), + onChanged: (val) { + WakelockPlus.toggle(enable: val); + MiruStorage.setSetting(SettingKey.enableWakelock, val); + }), + const SizedBox(height: 16), + ], + )), ); } diff --git a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart index 7dd0c4c9..162892eb 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart @@ -4,6 +4,9 @@ import 'package:get/get.dart'; import 'package:miru_app/controllers/watch/novel_controller.dart'; import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; +import 'package:miru_app/utils/miru_storage.dart'; +import 'package:miru_app/views/widgets/settings/settings_switch_tile.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; class NovelReaderSettings extends StatefulWidget { const NovelReaderSettings(this.tag, {super.key}); @@ -19,24 +22,32 @@ class _NovelReaderSettingsState extends State { Widget _buildAndroid(BuildContext context) { return Padding( padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text("novel-settings.font-size".i18n), - const SizedBox(height: 16), - Obx( - () => Slider( - value: _c.fontSize.value, - onChanged: (value) { - _c.fontSize.value = value; - }, - min: 12, - max: 24, - ), - ), - ], - ), + child: Obx(() => Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text("novel-settings.font-size".i18n), + const SizedBox(height: 16), + Slider( + value: _c.fontSize.value, + onChanged: (value) { + _c.fontSize.value = value; + }, + min: 12, + max: 24, + ), + const SizedBox(height: 16), + SettingsSwitchTile( + icon: const Icon(Icons.coffee), + title: "reader-settings.enable-wakelock".i18n, + buildValue: () => + MiruStorage.getSetting(SettingKey.enableWakelock), + onChanged: (val) { + WakelockPlus.toggle(enable: val); + MiruStorage.setSetting(SettingKey.enableWakelock, val); + }) + ], + )), ); } diff --git a/lib/views/widgets/watch/control_panel_header.dart b/lib/views/widgets/watch/control_panel_header.dart index 9c3a54fd..c27230c9 100644 --- a/lib/views/widgets/watch/control_panel_header.dart +++ b/lib/views/widgets/watch/control_panel_header.dart @@ -42,7 +42,13 @@ class _ControlPanelHeaderState onPressed: () { showModalBottomSheet( context: context, - builder: (context) => widget.buildSettings(context), + isScrollControlled: true, + builder: (context) => DraggableScrollableSheet( + expand: false, + builder: (context, controller) => SingleChildScrollView( + controller: controller, + child: widget.buildSettings(context)), + ), ); }, icon: const Icon(Icons.settings), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 882d01b7..5d9d8f79 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,7 @@ import FlutterMacOS import Foundation +import battery_plus import desktop_multi_window import device_info_plus import flutter_inappwebview_macos @@ -22,6 +23,7 @@ import wakelock_plus import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) FlutterMultiWindowPlugin.register(with: registry.registrar(forPlugin: "FlutterMultiWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) diff --git a/pubspec.lock b/pubspec.lock index ad2e560a..cfe0aa71 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -57,6 +57,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + based_battery_indicator: + dependency: "direct main" + description: + name: based_battery_indicator + sha256: "61c5b5a33e5dc35fc45cb016160f4eae8ee9a85c804bd797a5dbb9283bc9f288" + url: "https://pub.dev" + source: hosted + version: "1.0.3" + battery_plus: + dependency: "direct main" + description: + name: battery_plus + sha256: "0568fbba70697b8d0c34c1176faa2bc6d61c7fb211a2d2d64e493b91ff72d3f8" + url: "https://pub.dev" + source: hosted + version: "5.0.2" + battery_plus_platform_interface: + dependency: transitive + description: + name: battery_plus_platform_interface + sha256: "942707f90e2f7481dcb178df02e22a9c6971b3562b848d6a1b8c7cff9f1a1fec" + url: "https://pub.dev" + source: hosted + version: "2.0.0" boolean_selector: dependency: transitive description: @@ -1347,6 +1371,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0+1" + upower: + dependency: transitive + description: + name: upower + sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf + url: "https://pub.dev" + source: hosted + version: "0.7.0" uri_parser: dependency: transitive description: @@ -1444,7 +1476,7 @@ packages: source: hosted version: "2.0.7" wakelock_plus: - dependency: transitive + dependency: "direct main" description: name: wakelock_plus sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d diff --git a/pubspec.yaml b/pubspec.yaml index 914fe46b..7e3418b4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,6 +59,9 @@ dependencies: flutter_socks_proxy: ^0.0.3 logging: ^1.2.0 share_plus: ^7.2.1 + wakelock_plus: ^1.1.4 + battery_plus: ^5.0.2 + based_battery_indicator: ^1.0.3 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2a40c6ec..c9e9bd24 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,7 @@ #include "generated_plugin_registrant.h" +#include #include #include #include @@ -19,6 +20,8 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + BatteryPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); DesktopMultiWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin")); FlutterJsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 12c4f110..f3002f91 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + battery_plus desktop_multi_window flutter_js flutter_windows_webview From 450dbd7344c341c9a3d0f7cc72318d55d46b10fe Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sat, 20 Jan 2024 22:19:20 +0800 Subject: [PATCH 02/24] i18n --- .vscode/settings.json | 3 ++ assets/i18n/en.json | 15 +++++++++- lib/controllers/watch/comic_controller.dart | 8 ++--- .../reader/comic/comic_reader_content.dart | 10 +++---- .../reader/comic/comic_reader_settings.dart | 30 +++++++++++++++---- 5 files changed, 51 insertions(+), 15 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..f5854be8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "cmake.sourceDirectory": "C:/Users/USER/Documents/miru-app/windows/flutter" +} \ No newline at end of file diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 61d220d9..176a71c4 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -189,7 +189,20 @@ "read-mode": "Read mode", "standard": "Standard", "right-to-left": "Right to left", - "web-tonn": "Webtoon" + "web-tonn": "Webtoon", + "bottomLeft": "Bottom left", + "bottomRight": "Bottom right", + "topLeft": "Top left", + "topRight": "Top right", + "indicator-alignment": "Indicator alignment", + "status-bar":"Status bar" + }, + "reader-settings": { + "enable-wakelock": "Keep screen on", + "battery": "Battery", + "time": "Time", + "page-indicator": "Page Indicator", + "battery-icon": "Battery Icon" }, "novel-settings": { "font-size": "Font size" diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index aa9d360c..518a39fb 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -50,10 +50,10 @@ class ComicController extends ReaderController { // 是否按下 ctrl Timer? _barreryTimer; final statusBarElement = { - 'reader-setting.battery'.i18n: true.obs, - 'reader-setting.time'.i18n: true.obs, - 'reader-setting.page-indicator'.i18n: true.obs, - 'reader-setting.battery-icon'.i18n: true.obs, + 'reader-settings.battery'.i18n: true.obs, + 'reader-settings.time'.i18n: true.obs, + 'reader-settings.page-indicator'.i18n: true.obs, + 'reader-settings.battery-icon'.i18n: true.obs, }; final isZoom = false.obs; final currentTime = "".obs; diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 56f5d3b3..5a52c194 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -62,7 +62,7 @@ class _ComicReaderContentState extends State { Widget _indicatorBuilder() { return Obx(() => Row(mainAxisSize: MainAxisSize.min, children: [ - if (_c.statusBarElement["reader-setting.page-indicator".i18n]! + if (_c.statusBarElement["reader-settings.page-indicator".i18n]! .value) ...[ Text( "${_c.currentPage.value + 1}/${_c.watchData.value?.urls.length ?? 0}", @@ -70,8 +70,8 @@ class _ComicReaderContentState extends State { ), const SizedBox(width: 8) ], - if (_c - .statusBarElement["reader-setting.battery-icon".i18n]!.value) ...[ + if (_c.statusBarElement["reader-settings.battery-icon".i18n]! + .value) ...[ BasedBatteryIndicator( status: BasedBatteryStatus( value: _c.batteryLevel.value, @@ -84,14 +84,14 @@ class _ComicReaderContentState extends State { ), const SizedBox(width: 8) ], - if (_c.statusBarElement["reader-setting.battery".i18n]!.value) ...[ + if (_c.statusBarElement["reader-settings.battery".i18n]!.value) ...[ Text( "${_c.batteryLevel.value}%", style: const TextStyle(color: Colors.white, fontSize: 15), ), const SizedBox(width: 8) ], - if (_c.statusBarElement["reader-setting.time".i18n]!.value) ...[ + if (_c.statusBarElement["reader-settings.time".i18n]!.value) ...[ Text( _c.currentTime.value, style: const TextStyle(color: Colors.white, fontSize: 15), diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index ba7f7e03..fa4c471b 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -19,7 +19,6 @@ class ComicReaderSettings extends StatefulWidget { class _ComicReaderSettingsState extends State { late final ComicController _c = Get.find(tag: widget.tag); - Widget _buildAndroid(BuildContext context) { return Padding( padding: const EdgeInsets.all(16), @@ -67,19 +66,40 @@ class _ComicReaderSettingsState extends State { segments: [ ButtonSegment( value: Alignment.bottomLeft, - label: Text('comic-settings.bottomLeft'.i18n), + label: Column(children: [ + Text('comic-settings.bottomLeft'.i18n), + const SizedBox(height: 5), + Transform.rotate( + angle: -3.14, + child: const Icon(Icons.arrow_outward)) + ]), ), ButtonSegment( value: Alignment.bottomRight, - label: Text('comic-settings.rightLeft'.i18n), + label: Column(children: [ + Text('comic-settings.bottomRight'.i18n), + const SizedBox(height: 5), + Transform.rotate( + angle: 1.57, child: const Icon(Icons.arrow_outward)) + ]), ), ButtonSegment( value: Alignment.topLeft, - label: Text('comic-settings.topLeft'.i18n), + label: Column(children: [ + Text('comic-settings.topLeft'.i18n), + const SizedBox(height: 5), + Transform.rotate( + angle: -1.57, + child: const Icon(Icons.arrow_outward)) + ]), ), ButtonSegment( value: Alignment.topRight, - label: Text('comic-settings.topRight'.i18n), + label: Column(children: [ + Text('comic-settings.topRight'.i18n), + const SizedBox(height: 5), + const Icon(Icons.arrow_outward) + ]), ) ], selected: {_c.alignMode.value}, From c45fd3951ba39742c783357142b3f508cbc4b6e1 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 21 Jan 2024 16:50:38 +0800 Subject: [PATCH 03/24] feat:add tachiyomi style slider on manga readder --- lib/controllers/watch/comic_controller.dart | 24 +++- lib/controllers/watch/reader_controller.dart | 8 +- .../reader/comic/comic_reader_content.dart | 1 + .../widgets/watch/control_panel_footer.dart | 126 ++++++++++++------ 4 files changed, 114 insertions(+), 45 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 518a39fb..19f8df5d 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -31,18 +31,15 @@ class ComicController extends ReaderController { 'webTonn': MangaReadMode.webTonn, }; final String setting = MiruStorage.getSetting(SettingKey.readingMode); - final readType = MangaReadMode.standard.obs; - final currentScale = 1.0.obs; - // MangaReadMode // 当前页码 final currentPage = 0.obs; - final pageController = ExtendedPageController().obs; final itemPositionsListener = ItemPositionsListener.create(); final itemScrollController = ItemScrollController(); final scrollOffsetController = ScrollOffsetController(); + final scrollOffsetListener = ScrollOffsetListener.create(); final alignMode = Alignment.bottomLeft.obs; // 是否已经恢复上次阅读 final isRecover = false.obs; @@ -64,7 +61,6 @@ class ComicController extends ReaderController { final hour = datenow.hour < 10 ? "0${datenow.hour}" : datenow.hour; final minute = datenow.minute < 10 ? "0${datenow.minute}" : datenow.minute; currentTime.value = "$hour:$minute"; - debugPrint("${datenow.toLocal()}"); } @override @@ -83,6 +79,10 @@ class ComicController extends ReaderController { final pos = itemPositionsListener.itemPositions.value.first; currentPage.value = pos.index; }); + scrollOffsetListener.changes.listen((event) { + // debugPrint("offset"); + hideControlPanel(); + }); ever(readType, (callback) { _jumpPage(currentPage.value); @@ -94,11 +94,23 @@ class ComicController extends ReaderController { }); // 如果切换章节,重置当前页码 ever(super.index, (callback) => currentPage.value = 0); + //control footer 的 slider 改變時,更新頁碼 + ever(progress, (callback) { + // 防止逆向回饋 + if (!updateSlider.value) { + return; + } + currentPage.value = callback; + _jumpPage(callback); + }); + ever(currentPage, (callback) { + progress.value = callback; + updateSlider.value = false; + }); ever(super.watchData, (callback) async { if (isRecover.value || callback == null) { return; } - isRecover.value = true; // 获取上次阅读的页码 final history = await DatabaseService.getHistoryByPackageAndUrl( diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index 97bac28b..8c87db95 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -32,9 +32,11 @@ class ReaderController extends GetxController { final error = ''.obs; final isShowControlPanel = false.obs; late final index = playIndex.obs; + late final progress = 0.obs; get cuurentPlayUrl => playList[index.value].url; Timer? _timer; - + final isScrolled = true.obs; + final updateSlider = true.obs; @override void onInit() { getContent(); @@ -64,6 +66,10 @@ class ReaderController extends GetxController { }); } + hideControlPanel() { + isShowControlPanel.value = false; + } + addHistory(String progress, String totalProgress) async { await DatabaseService.putHistory( History() diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 5a52c194..eed60a5d 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -176,6 +176,7 @@ class _ComicReaderContentState extends State { itemScrollController: _c.itemScrollController, itemPositionsListener: _c.itemPositionsListener, scrollOffsetController: _c.scrollOffsetController, + scrollOffsetListener: _c.scrollOffsetListener, itemBuilder: (context, index) { final url = images[index]; return CacheNetWorkImagePic( diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index de165cd6..2e2a7d96 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -4,50 +4,100 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; -import 'package:miru_app/utils/i18n.dart'; -import 'package:miru_app/views/widgets/button.dart'; +import 'package:miru_app/controllers/watch/novel_controller.dart'; class ControlPanelFooter extends StatelessWidget { const ControlPanelFooter(this.tag, {super.key}); final String tag; - @override Widget build(BuildContext context) { final c = Get.find(tag: tag); - return Container( - height: 80, - padding: const EdgeInsets.symmetric(horizontal: 20), - decoration: BoxDecoration( - color: Platform.isAndroid - ? Theme.of(context).colorScheme.background.withOpacity(0.9) - : Colors.transparent, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(40), - topRight: Radius.circular(40), - ), - ), - clipBehavior: Clip.antiAlias, - child: Obx( - () => Row( - children: [ - if (c.index.value > 0) - PlatformFilledButton( - child: Text('common.previous'.i18n), - onPressed: () { - c.index.value--; - }, - ), - const Spacer(), - if (c.index.value != c.playList.length - 1) - PlatformFilledButton( - child: Text('common.next'.i18n), - onPressed: () { - c.index.value++; - }, - ), - ], - ), - ), - ).animate().fade(); + final int total = (T == NovelController) + ? c.watchData.value?.content.length ?? 0 + : c.watchData.value?.urls.length ?? 0; + final totalObs = total.obs; + final progressObs = c.progress.value.obs; + ever(c.watchData, (callback) { + progressObs.value = 0; + totalObs.value = (T == NovelController) + ? c.watchData.value?.content.length ?? 0 + : c.watchData.value?.urls.length ?? 0; + }); + ever(c.progress, (callback) { + progressObs.value = callback; + }); + ever(c.isShowControlPanel, (callback) { + debugPrint("scrolled ${c.progress.value}"); + progressObs.value = c.progress.value; + }); + final double width = MediaQuery.of(context).size.width; + final Color containerColor = Platform.isAndroid + ? Theme.of(context).colorScheme.background.withOpacity(0.9) + : Colors.transparent; + + return GestureDetector( + child: SizedBox( + height: 110, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 15), + child: Obx(() => Row(children: [ + const SizedBox( + height: 10, + ), + if (c.index.value > 0) + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: containerColor, + ), + child: IconButton( + onPressed: () { + c.index.value--; + }, + icon: const Icon(Icons.skip_previous_rounded))), + const Spacer(), + SizedBox( + height: 50, + width: width * 2 / 3, + child: Material( + color: containerColor, + borderRadius: BorderRadius.circular(30), + child: Obx(() { + if (totalObs.value != 0 || + !c.isShowControlPanel.value) { + return Slider( + label: (progressObs.value + 1).toString(), + max: (totalObs.value - 1).toDouble(), + min: 0, + divisions: totalObs.value, + value: progressObs.value.toDouble(), + onChanged: c.isShowControlPanel.value + ? (val) { + c.updateSlider.value = true; + c.progress.value = val.toInt(); + } + : null, + ); + } + return const Slider(value: 0, onChanged: null); + }))), + const Spacer(), + if (c.index.value != c.playList.length - 1) + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: containerColor, + ), + child: IconButton( + onPressed: () { + c.index.value++; + }, + icon: const Icon(Icons.skip_next_rounded))) + ]))).animate().slideY(begin: 1, end: 0), + )); } } From 4123b7b81e0333e785a8937c6fe2e455299b9dcd Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 21 Jan 2024 17:55:12 +0800 Subject: [PATCH 04/24] feat:support novel --- lib/controllers/watch/comic_controller.dart | 4 -- lib/controllers/watch/novel_controller.dart | 38 +++++++++++++++++++ lib/controllers/watch/reader_controller.dart | 6 ++- .../reader/novel/novel_reader_content.dart | 2 + .../widgets/watch/control_panel_footer.dart | 2 +- 5 files changed, 45 insertions(+), 7 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 19f8df5d..645e055a 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -37,9 +37,6 @@ class ComicController extends ReaderController { final currentPage = 0.obs; final pageController = ExtendedPageController().obs; final itemPositionsListener = ItemPositionsListener.create(); - final itemScrollController = ItemScrollController(); - final scrollOffsetController = ScrollOffsetController(); - final scrollOffsetListener = ScrollOffsetListener.create(); final alignMode = Alignment.bottomLeft.obs; // 是否已经恢复上次阅读 final isRecover = false.obs; @@ -80,7 +77,6 @@ class ComicController extends ReaderController { currentPage.value = pos.index; }); scrollOffsetListener.changes.listen((event) { - // debugPrint("offset"); hideControlPanel(); }); diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index cb0e4b69..f3d78e78 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -46,7 +46,38 @@ class NovelController extends ReaderController { // 切换章节时重置页码 ever(index, (callback) => positions.value = 0); + ever(super.watchData, (callback) async { + if (isRecover.value || callback == null) { + return; + } + isRecover.value = true; + // 获取上次阅读的页码 + final history = await DatabaseService.getHistoryByPackageAndUrl( + super.runtime.extension.package, + super.detailUrl, + ); + if (history == null || + history.progress.isEmpty || + episodeGroupId != history.episodeGroupId || + history.episodeId != index.value) { + return; + } + positions.value = int.parse(history.progress); + _jumpLine(positions.value); + }); + ever(progress, (callback) { + // 防止逆向回饋 + if (!updateSlider.value) { + return; + } + positions.value = callback; + _jumpLine(callback); + }); + ever(positions, (callback) { + progress.value = callback; + updateSlider.value = false; + }); ever(super.watchData, (callback) async { if (isRecover.value || callback == null) { return; @@ -67,6 +98,13 @@ class NovelController extends ReaderController { }); } + _jumpLine(int? index) { + if (index == null) { + return; + } + itemScrollController.jumpTo(index: index); + } + @override void onClose() { if (super.watchData.value != null) { diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index 8c87db95..d7c248ad 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -1,11 +1,11 @@ import 'dart:async'; - import 'package:get/get.dart'; import 'package:miru_app/models/extension.dart'; import 'package:miru_app/models/history.dart'; import 'package:miru_app/controllers/home_controller.dart'; import 'package:miru_app/data/services/database_service.dart'; import 'package:miru_app/data/services/extension_service.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class ReaderController extends GetxController { final String title; @@ -16,7 +16,9 @@ class ReaderController extends GetxController { final ExtensionService runtime; final String? cover; final String anilistID; - + final scrollOffsetController = ScrollOffsetController(); + final scrollOffsetListener = ScrollOffsetListener.create(); + final itemScrollController = ItemScrollController(); ReaderController({ required this.title, required this.playList, diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index e8460c99..8c3da390 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -61,6 +61,8 @@ class _NovelReaderContentState extends State { child: ScrollablePositionedList.builder( itemPositionsListener: _c.itemPositionsListener, initialScrollIndex: _c.positions.value, + itemScrollController: _c.itemScrollController, + scrollOffsetController: _c.scrollOffsetController, padding: EdgeInsets.symmetric( horizontal: listviewPadding, vertical: 16, diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index 2e2a7d96..3d55a3bd 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -71,7 +71,7 @@ class ControlPanelFooter extends StatelessWidget { label: (progressObs.value + 1).toString(), max: (totalObs.value - 1).toDouble(), min: 0, - divisions: totalObs.value, + divisions: totalObs.value - 1, value: progressObs.value.toDouble(), onChanged: c.isShowControlPanel.value ? (val) { From 3f967bca62bdbe95c1a48dbdeab07b0f960add42 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Mon, 22 Jan 2024 17:47:31 +0800 Subject: [PATCH 05/24] improve manga reader feat: configurable tap zone autoscroller save image from comic by doubletap --- lib/controllers/watch/comic_controller.dart | 15 +- lib/controllers/watch/reader_controller.dart | 39 +++ lib/utils/miru_storage.dart | 8 + .../reader/comic/comic_reader_content.dart | 53 ++- .../reader/comic/comic_reader_settings.dart | 331 ++++++++++++------ .../reader/novel/novel_reader_settings.dart | 2 + lib/views/widgets/cache_network_image.dart | 119 ++++--- .../widgets/watch/control_panel_footer.dart | 5 +- lib/views/widgets/watch/reader_view.dart | 71 ++-- 9 files changed, 447 insertions(+), 196 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 645e055a..85b4461f 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -41,7 +41,6 @@ class ComicController extends ReaderController { // 是否已经恢复上次阅读 final isRecover = false.obs; final batteryLevel = 100.obs; - // 是否按下 ctrl Timer? _barreryTimer; final statusBarElement = { 'reader-settings.battery'.i18n: true.obs, @@ -51,7 +50,7 @@ class ComicController extends ReaderController { }; final isZoom = false.obs; final currentTime = "".obs; - Future _statusBar([Timer? t]) async { + Future _statusBar() async { final battery = Battery(); batteryLevel.value = await battery.batteryLevel; final datenow = DateTime.now(); @@ -67,8 +66,8 @@ class ComicController extends ReaderController { WakelockPlus.toggle( enable: MiruStorage.getSetting(SettingKey.enableWakelock)); await _statusBar(); - _barreryTimer = Timer.periodic( - const Duration(seconds: 10), (timer) => _statusBar(timer)); + _barreryTimer = + Timer.periodic(const Duration(seconds: 10), (timer) => _statusBar()); itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; @@ -79,7 +78,9 @@ class ComicController extends ReaderController { scrollOffsetListener.changes.listen((event) { hideControlPanel(); }); - + ever(height, (callback) { + super.height.value = callback; + }); ever(readType, (callback) { _jumpPage(currentPage.value); // 保存设置 @@ -224,6 +225,10 @@ class ComicController extends ReaderController { pages.toString(), ); } + //check auto scroller is closed or not + if (autoScrollTimer != null) { + autoScrollTimer!.cancel(); + } if (MiruStorage.getSetting(SettingKey.autoTracking) && anilistID != "") { AniListProvider.editList( status: AnilistMediaListStatus.current, diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index d7c248ad..f55daada 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -1,10 +1,12 @@ import 'dart:async'; import 'package:get/get.dart'; +import 'package:flutter/material.dart'; import 'package:miru_app/models/extension.dart'; import 'package:miru_app/models/history.dart'; import 'package:miru_app/controllers/home_controller.dart'; import 'package:miru_app/data/services/database_service.dart'; import 'package:miru_app/data/services/extension_service.dart'; +import 'package:miru_app/utils/miru_storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; class ReaderController extends GetxController { @@ -37,12 +39,49 @@ class ReaderController extends GetxController { late final progress = 0.obs; get cuurentPlayUrl => playList[index.value].url; Timer? _timer; + Timer? autoScrollTimer; final isScrolled = true.obs; final updateSlider = true.obs; + //點擊區域是否反轉 + final RxBool tapRegionIsReversed = false.obs; + final dynamic _nextPageHitBox = + MiruStorage.getSetting(SettingKey.nextPageHitBox); + final double _prevPageHitBox = + MiruStorage.getSetting(SettingKey.prevPageHitBox); + final int _autoScrollInterval = + MiruStorage.getSetting(SettingKey.autoScrollInterval); + final double _autoScrollOffset = + MiruStorage.getSetting(SettingKey.autoScrollOffset); + final RxInt autoScrollInterval = 300.obs; + final RxDouble autoScrollOffset = 0.4.obs; + final RxDouble nextPageHitBox = 0.3.obs; + final RxDouble prevPageHitBox = 0.3.obs; + final enableAutoScroll = false.obs; + final height = 1000.0.obs; @override void onInit() { getContent(); + autoScrollInterval.value = _autoScrollInterval; + autoScrollOffset.value = _autoScrollOffset; + nextPageHitBox.value = _nextPageHitBox; + prevPageHitBox.value = _prevPageHitBox; ever(index, (callback) => getContent()); + ever(enableAutoScroll, (callback) { + if (callback) { + autoScrollTimer = Timer.periodic( + Duration(milliseconds: autoScrollInterval.value), (timer) { + if (isScrolled.value) { + scrollOffsetController.animateScroll( + duration: const Duration(milliseconds: 100), + curve: Curves.ease, + offset: autoScrollOffset.value, + ); + } + }); + return; + } + autoScrollTimer?.cancel(); + }); super.onInit(); } diff --git a/lib/utils/miru_storage.dart b/lib/utils/miru_storage.dart index e4f80242..06978515 100644 --- a/lib/utils/miru_storage.dart +++ b/lib/utils/miru_storage.dart @@ -130,6 +130,10 @@ class MiruStorage { await _initSetting(SettingKey.proxyType, 'DIRECT'); await _initSetting(SettingKey.saveLog, true); await _initSetting(SettingKey.enableWakelock, false); + await _initSetting(SettingKey.nextPageHitBox, 0.2); + await _initSetting(SettingKey.prevPageHitBox, 0.2); + await _initSetting(SettingKey.autoScrollInterval, 300); + await _initSetting(SettingKey.autoScrollOffset, 20.0); } static _initSetting(String key, dynamic value) async { @@ -190,4 +194,8 @@ class SettingKey { static String proxy = "Proxy"; static String proxyType = "ProxyType"; static String saveLog = "SaveLog"; + static String nextPageHitBox = "NextPageHitBox"; + static String prevPageHitBox = "PrevPageHitBox"; + static String autoScrollInterval = "AutoScrollInterval"; + static String autoScrollOffset = "AutoScrollOffset"; } diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index eed60a5d..9f28af57 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -179,12 +179,7 @@ class _ComicReaderContentState extends State { scrollOffsetListener: _c.scrollOffsetListener, itemBuilder: (context, index) { final url = images[index]; - return CacheNetWorkImagePic( - url, - fit: BoxFit.fitWidth, - placeholder: _buildPlaceholder(context), - headers: _c.watchData.value?.headers, - ); + return imageBuilder(url); }, itemCount: images.length, ), @@ -208,14 +203,7 @@ class _ComicReaderContentState extends State { padding: EdgeInsets.symmetric( horizontal: viewPadding, ), - child: CacheNetWorkImagePic( - url, - mode: ExtendedImageMode.gesture, - key: ValueKey(url), - fit: BoxFit.contain, - placeholder: _buildPlaceholder(context), - headers: _c.watchData.value?.headers, - ), + child: imageBuilder(url), ); }, ); @@ -226,8 +214,45 @@ class _ComicReaderContentState extends State { ); } + Widget imageBuilder(String url) { + return GestureDetector( + onTapDown: (deatils) { + _c.isShowControlPanel.value = !_c.isShowControlPanel.value; + }, + onDoubleTapDown: (details) { + showModalBottomSheet( + context: context, + showDragHandle: true, + useSafeArea: true, + builder: (_) => SizedBox( + height: 100, + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.save), + title: Text('common.save'.i18n), + onTap: () { + Navigator.of(context).pop(); + saveImage( + url, _c.watchData.value?.headers, mounted, context); + }, + ), + ], + ), + ), + ); + }, + child: CacheNetWorkImagePic( + url, + fit: BoxFit.fitWidth, + placeholder: _buildPlaceholder(context), + headers: _c.watchData.value?.headers, + )); + } + @override Widget build(BuildContext context) { + _c.height.value = MediaQuery.of(context).size.height; return PlatformBuildWidget( androidBuilder: (context) { return Scaffold( diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index fa4c471b..e623ccb1 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -1,5 +1,6 @@ import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:flutter/material.dart'; +// import 'package:flutter_box_transform/flutter_box_transform.dart'; import 'package:get/get.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/comic_controller.dart'; @@ -7,6 +8,7 @@ import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/utils/miru_storage.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/settings/settings_switch_tile.dart'; +import 'package:miru_app/views/widgets/settings/settings_tile.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; class ComicReaderSettings extends StatefulWidget { @@ -19,125 +21,224 @@ class ComicReaderSettings extends StatefulWidget { class _ComicReaderSettingsState extends State { late final ComicController _c = Get.find(tag: widget.tag); - Widget _buildAndroid(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Obx(() => Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // 阅读模式 - Text('comic-settings.read-mode'.i18n), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: SegmentedButton( - segments: [ - ButtonSegment( - value: MangaReadMode.standard, - label: Text('comic-settings.standard'.i18n), - ), - ButtonSegment( - value: MangaReadMode.rightToLeft, - label: Text('comic-settings.right-to-left'.i18n), - ), - ButtonSegment( - value: MangaReadMode.webTonn, - label: Text('comic-settings.web-tonn'.i18n), - ), - ], - selected: {_c.readType.value}, - onSelectionChanged: (value) { - if (value.isNotEmpty) { - _c.readType.value = value.first; - } - }, - showSelectedIcon: false, - ), + // double nextPageHitBox = MiruStorage.getSetting(SettingKey.nextPageHitBox); + // double prevPageHitBox = MiruStorage.getSetting(SettingKey.prevPageHitBox); + Widget _buildAndroid(BuildContext context) { + return DefaultTabController( + length: 2, + child: Column( + children: [ + TabBar(tabs: [ + Tab( + text: "Common".i18n, ), + Tab( + text: "Webtoon".i18n, + ) + ]), + SizedBox( + height: MediaQuery.of(context).size.height, + child: TabBarView(children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 阅读模式 - const SizedBox(height: 16), - Text('comic-settings.indicator-alignment'.i18n), - const SizedBox(height: 16), - SizedBox( - width: double.infinity, - child: SegmentedButton( - segments: [ - ButtonSegment( - value: Alignment.bottomLeft, - label: Column(children: [ - Text('comic-settings.bottomLeft'.i18n), - const SizedBox(height: 5), - Transform.rotate( - angle: -3.14, - child: const Icon(Icons.arrow_outward)) - ]), - ), - ButtonSegment( - value: Alignment.bottomRight, - label: Column(children: [ - Text('comic-settings.bottomRight'.i18n), - const SizedBox(height: 5), - Transform.rotate( - angle: 1.57, child: const Icon(Icons.arrow_outward)) - ]), - ), - ButtonSegment( - value: Alignment.topLeft, - label: Column(children: [ - Text('comic-settings.topLeft'.i18n), - const SizedBox(height: 5), - Transform.rotate( - angle: -1.57, - child: const Icon(Icons.arrow_outward)) - ]), - ), - ButtonSegment( - value: Alignment.topRight, - label: Column(children: [ - Text('comic-settings.topRight'.i18n), - const SizedBox(height: 5), - const Icon(Icons.arrow_outward) - ]), - ) - ], - selected: {_c.alignMode.value}, - onSelectionChanged: (value) { - if (value.isNotEmpty) { - _c.alignMode.value = value.first; - } - }, - showSelectedIcon: false, + Text('comic-settings.read-mode'.i18n), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: [ + ButtonSegment( + value: MangaReadMode.standard, + label: Text('comic-settings.standard'.i18n), + ), + ButtonSegment( + value: MangaReadMode.rightToLeft, + label: + Text('comic-settings.right-to-left'.i18n), + ), + ButtonSegment( + value: MangaReadMode.webTonn, + label: Text('comic-settings.web-tonn'.i18n), + ), + ], + selected: {_c.readType.value}, + onSelectionChanged: (value) { + if (value.isNotEmpty) { + _c.readType.value = value.first; + if (value.first == + MangaReadMode.rightToLeft) { + _c.tapRegionIsReversed.value = true; + return; + } + _c.tapRegionIsReversed.value = false; + } + }, + showSelectedIcon: false, + ), + ), + + const SizedBox(height: 16), + Text('comic-settings.indicator-alignment'.i18n), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: [ + ButtonSegment( + value: Alignment.bottomLeft, + label: Column(children: [ + Text('comic-settings.bottomLeft'.i18n), + const SizedBox(height: 5), + Transform.rotate( + angle: -3.14, + child: const Icon(Icons.arrow_outward)) + ]), + ), + ButtonSegment( + value: Alignment.bottomRight, + label: Column(children: [ + Text('comic-settings.bottomRight'.i18n), + const SizedBox(height: 5), + Transform.rotate( + angle: 1.57, + child: const Icon(Icons.arrow_outward)) + ]), + ), + ButtonSegment( + value: Alignment.topLeft, + label: Column(children: [ + Text('comic-settings.topLeft'.i18n), + const SizedBox(height: 5), + Transform.rotate( + angle: -1.57, + child: const Icon(Icons.arrow_outward)) + ]), + ), + ButtonSegment( + value: Alignment.topRight, + label: Column(children: [ + Text('comic-settings.topRight'.i18n), + const SizedBox(height: 5), + const Icon(Icons.arrow_outward) + ]), + ) + ], + selected: {_c.alignMode.value}, + onSelectionChanged: (value) { + if (value.isNotEmpty) { + _c.alignMode.value = value.first; + } + }, + showSelectedIcon: false, + ), + ), + const SizedBox(height: 16), + Text('comic-settings.status-bar'.i18n), + const SizedBox(height: 5), + Wrap( + spacing: 5, + children: _c.statusBarElement.keys + .map((e) => FilterChip( + label: Text(e), + selected: _c.statusBarElement[e]!.value, + onSelected: (val) { + _c.statusBarElement[e]!.value = val; + })) + .toList(), + ), + const SizedBox(height: 16), + Text('comic-settings.nextPageHitBox'.i18n), + Slider( + value: _c.nextPageHitBox.value, + max: 0.5, + divisions: 20, + label: _c.nextPageHitBox.toString(), + onChanged: (val) { + setState(() { + _c.nextPageHitBox.value = val; + }); + MiruStorage.setSetting( + SettingKey.nextPageHitBox, val); + }), + const SizedBox(height: 16), + Text('comic-settings.prevPageHitBox'.i18n), + Slider( + value: _c.prevPageHitBox.value, + max: 0.5, + divisions: 20, + label: _c.prevPageHitBox.toString(), + onChanged: (val) { + setState(() { + _c.prevPageHitBox.value = val; + }); + MiruStorage.setSetting( + SettingKey.prevPageHitBox, val); + }), + SettingsSwitchTile( + icon: const Icon(Icons.coffee), + title: "reader-settings.enable-wakelock".i18n, + buildValue: () => MiruStorage.getSetting( + SettingKey.enableWakelock), + onChanged: (val) { + WakelockPlus.toggle(enable: val); + MiruStorage.setSetting( + SettingKey.enableWakelock, val); + }), + ], + )), ), - ), - const SizedBox(height: 16), - Text('comic-settings.status-bar'.i18n), - const SizedBox(height: 5), - Wrap( - spacing: 5, - children: _c.statusBarElement.keys - .map((e) => FilterChip( - label: Text(e), - selected: _c.statusBarElement[e]!.value, - onSelected: (val) { - _c.statusBarElement[e]!.value = val; - })) - .toList(), - ), - SettingsSwitchTile( - icon: const Icon(Icons.coffee), - title: "reader-settings.enable-wakelock".i18n, - buildValue: () => - MiruStorage.getSetting(SettingKey.enableWakelock), - onChanged: (val) { - WakelockPlus.toggle(enable: val); - MiruStorage.setSetting(SettingKey.enableWakelock, val); - }), - const SizedBox(height: 16), - ], - )), - ); + Padding( + padding: const EdgeInsets.all(16), + child: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSwitchTile( + icon: const Icon(Icons.play_arrow_rounded), + title: "reader-settings.enable-autoscroller".i18n, + buildValue: () => _c.enableAutoScroll.value, + onChanged: (val) { + Get.back(); + _c.enableAutoScroll.value = val; + }), + const SizedBox(height: 16), + Text('reader-settings.auto-scroller-interval'.i18n), + Slider( + value: _c.autoScrollInterval.value.toDouble(), + max: 500.0, + divisions: 25, + label: "${_c.autoScrollInterval} ms", + onChanged: (val) { + _c.autoScrollInterval.value = val.toInt(); + MiruStorage.setSetting( + SettingKey.autoScrollInterval, val.toInt()); + }), + const SizedBox(height: 16), + Text('reader-settings.auto-scroller-offset'.i18n), + Slider( + value: _c.autoScrollOffset.value, + max: 300.0, + divisions: 30, + label: "${_c.autoScrollOffset} pixels", + onChanged: (val) { + _c.autoScrollOffset.value = val; + MiruStorage.setSetting( + SettingKey.autoScrollOffset, val); + }), + ], + )), + ) + ]), + ) + ], + )); } Widget _buildDesktop(BuildContext context) { diff --git a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart index 162892eb..e7b76069 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart @@ -30,9 +30,11 @@ class _NovelReaderSettingsState extends State { const SizedBox(height: 16), Slider( value: _c.fontSize.value, + label: _c.fontSize.value.toString(), onChanged: (value) { _c.fontSize.value = value; }, + divisions: 12, min: 12, max: 24, ), diff --git a/lib/views/widgets/cache_network_image.dart b/lib/views/widgets/cache_network_image.dart index 3aa5a9b6..8b5d413d 100644 --- a/lib/views/widgets/cache_network_image.dart +++ b/lib/views/widgets/cache_network_image.dart @@ -91,6 +91,46 @@ class CacheNetWorkImagePic extends StatelessWidget { } } +void saveImage(url, Map? headers, bool mounted, + BuildContext context) async { + // final url = widget.url; + final fileName = url.split('/').last; + final res = await dio.get( + url, + options: Options( + responseType: ResponseType.bytes, + headers: headers, + ), + ); + if (Platform.isAndroid) { + final result = await ImageGallerySaver.saveImage( + res.data, + name: fileName, + ); + if (mounted) { + final msg = result['isSuccess'] == true + ? 'common.save-success'.i18n + : result['errorMessage']; + showPlatformSnackbar( + context: context, + content: msg, + ); + } + return; + } + // 打开目录选择对话框file_picker + + final path = await FilePicker.platform.saveFile( + type: FileType.image, + fileName: fileName, + ); + if (path == null) { + return; + } + // 保存 + File(path).writeAsBytesSync(res.data); +} + class _ThumnailPage extends StatefulWidget { const _ThumnailPage({ required this.url, @@ -113,44 +153,44 @@ class _ThumnailPageState extends State<_ThumnailPage> { super.dispose(); } - _saveImage() async { - final url = widget.url; - final fileName = url.split('/').last; - final res = await dio.get( - url, - options: Options( - responseType: ResponseType.bytes, - headers: widget.headers, - ), - ); - if (Platform.isAndroid) { - final result = await ImageGallerySaver.saveImage( - res.data, - name: fileName, - ); - if (mounted) { - final msg = result['isSuccess'] == true - ? 'common.save-success'.i18n - : result['errorMessage']; - showPlatformSnackbar( - context: context, - content: msg, - ); - } - return; - } - // 打开目录选择对话框file_picker + // _saveImage() async { + // final url = widget.url; + // final fileName = url.split('/').last; + // final res = await dio.get( + // url, + // options: Options( + // responseType: ResponseType.bytes, + // headers: widget.headers, + // ), + // ); + // if (Platform.isAndroid) { + // final result = await ImageGallerySaver.saveImage( + // res.data, + // name: fileName, + // ); + // if (mounted) { + // final msg = result['isSuccess'] == true + // ? 'common.save-success'.i18n + // : result['errorMessage']; + // showPlatformSnackbar( + // context: context, + // content: msg, + // ); + // } + // return; + // } + // // 打开目录选择对话框file_picker - final path = await FilePicker.platform.saveFile( - type: FileType.image, - fileName: fileName, - ); - if (path == null) { - return; - } - // 保存 - File(path).writeAsBytesSync(res.data); - } + // final path = await FilePicker.platform.saveFile( + // type: FileType.image, + // fileName: fileName, + // ); + // if (path == null) { + // return; + // } + // // 保存 + // File(path).writeAsBytesSync(res.data); + // } Widget _buildContent(BuildContext context) { return Center( @@ -193,6 +233,7 @@ class _ThumnailPageState extends State<_ThumnailPage> { appBar: AppBar(), body: GestureDetector( child: _buildContent(context), + onTapDown: (details) {}, onLongPress: () { showModalBottomSheet( context: context, @@ -207,7 +248,7 @@ class _ThumnailPageState extends State<_ThumnailPage> { title: Text('common.save'.i18n), onTap: () { Navigator.of(context).pop(); - _saveImage(); + saveImage(widget.url, widget.headers, mounted, context); }, ), ], @@ -238,7 +279,7 @@ class _ThumnailPageState extends State<_ThumnailPage> { text: Text('common.save'.i18n), onPressed: () { fluent.Flyout.of(context).close(); - _saveImage(); + saveImage(widget.url, widget.headers, mounted, context); }, ), ]); diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index 3d55a3bd..c67669b3 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -35,8 +35,7 @@ class ControlPanelFooter extends StatelessWidget { ? Theme.of(context).colorScheme.background.withOpacity(0.9) : Colors.transparent; - return GestureDetector( - child: SizedBox( + return SizedBox( height: 110, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15), @@ -98,6 +97,6 @@ class ControlPanelFooter extends StatelessWidget { }, icon: const Icon(Icons.skip_next_rounded))) ]))).animate().slideY(begin: 1, end: 0), - )); + ); } } diff --git a/lib/views/widgets/watch/reader_view.dart b/lib/views/widgets/watch/reader_view.dart index 5bc02245..df116471 100644 --- a/lib/views/widgets/watch/reader_view.dart +++ b/lib/views/widgets/watch/reader_view.dart @@ -19,6 +19,7 @@ class ReaderView extends StatelessWidget { @override Widget build(BuildContext context) { final c = Get.find(tag: tag); + final width = LayoutUtils.width; return Obx( () => Stack( children: [ @@ -36,28 +37,52 @@ class ReaderView extends StatelessWidget { // 点击中间显示控制面板 // 左边上一页右边下一页 - if (c.error.value.isEmpty) - Positioned( - top: 120, - bottom: 120, - left: 0, - right: 0, - child: GestureDetector( - onTapDown: (TapDownDetails details) { - final xPos = details.globalPosition.dx; - final width = LayoutUtils.width; - final unitWidth = width / 3; - if (xPos < unitWidth) { + if (c.error.value.isEmpty) ...[ + Padding( + padding: EdgeInsets.fromLTRB( + 0, 120, width - c.prevPageHitBox.value * width, 120), + child: GestureDetector( + onTapDown: (details) { + if (c.tapRegionIsReversed.value) { + return c.nextPage(); + } return c.previousPage(); - } - if (xPos > unitWidth * 2) { + }, + )), + Padding( + padding: EdgeInsets.fromLTRB( + width - c.nextPageHitBox.value * width, 120, 0, 120), + child: GestureDetector( + onTapDown: (details) { + if (c.tapRegionIsReversed.value) { + return c.previousPage(); + } return c.nextPage(); - } - c.isShowControlPanel.value = !c.isShowControlPanel.value; - }, - ), - ), + }, + )) + ] + // Positioned( + // top: 120, + // bottom: 120, + // left: 0, + // right: 0, + // child: GestureDetector( + // onTapDown: (TapDownDetails details) { + // final xPos = details.globalPosition.dx; + // final width = LayoutUtils.width; + // // final unitWidth = width / 3; + // if (xPos < c.prevPageHitBox.value * width) { + // return c.previousPage(); + // } + // if (xPos > width - c.nextPageHitBox.value * width) { + // return c.nextPage(); + // } + // c.isShowControlPanel.value = !c.isShowControlPanel.value; + // }, + // ), + // ), + , if (c.isShowControlPanel.value) ...[ // 顶部控制 Positioned( @@ -73,7 +98,13 @@ class ReaderView extends StatelessWidget { bottom: 0, child: ControlPanelFooter(tag), ), - ] + ], + if (c.enableAutoScroll.value) + ElevatedButton( + onPressed: () { + c.enableAutoScroll.value = false; + }, + child: const Icon(Icons.stop)), ], ), ); From 9bb4dba65a11b2e80aec37aede8b5d93033d0286 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Fri, 26 Jan 2024 00:12:59 +0800 Subject: [PATCH 06/24] add animations for novel --- .vscode/settings.json | 3 - lib/controllers/watch/novel_controller.dart | 3 + lib/controllers/watch/reader_controller.dart | 4 + .../reader/comic/comic_reader_content.dart | 2 +- .../reader/novel/novel_reader_content.dart | 360 +++++++++--------- .../widgets/watch/control_panel_footer.dart | 140 +++---- lib/views/widgets/watch/reader_view.dart | 7 +- 7 files changed, 267 insertions(+), 252 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index f5854be8..00000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "cmake.sourceDirectory": "C:/Users/USER/Documents/miru-app/windows/flutter" -} \ No newline at end of file diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index f3d78e78..814f65c2 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -39,6 +39,9 @@ class NovelController extends ReaderController { final pos = itemPositionsListener.itemPositions.value.first; positions.value = pos.index; }); + scrollOffsetListener.changes.listen((event) { + hideControlPanel(); + }); ever( fontSize, (callback) => MiruStorage.setSetting(SettingKey.novelFontSize, callback), diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index f55daada..7e3f21d5 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -100,6 +100,10 @@ class ReaderController extends GetxController { void nextPage() {} showControlPanel() { + if (isShowControlPanel.value) { + hideControlPanel(); + return; + } isShowControlPanel.value = true; _timer?.cancel(); _timer = Timer(const Duration(seconds: 3), () { diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 9f28af57..2cd4075b 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -217,7 +217,7 @@ class _ComicReaderContentState extends State { Widget imageBuilder(String url) { return GestureDetector( onTapDown: (deatils) { - _c.isShowControlPanel.value = !_c.isShowControlPanel.value; + _c.showControlPanel(); }, onDoubleTapDown: (details) { showModalBottomSheet( diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index 8c3da390..e2e9f374 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -20,186 +20,190 @@ class _NovelReaderContentState extends State { late final _c = Get.find(tag: widget.tag); _buildContent() { - return LayoutBuilder( - builder: (context, constraints) => Obx( - () { - // // 宽度 大于 800 就是整体宽度的一半 - final maxWidth = constraints.maxWidth; - // final width = maxWidth > 800 ? maxWidth / 2 : maxWidth; - // final height = constraints.maxHeight; - if (_c.error.value.isNotEmpty) { - return SizedBox( - width: double.infinity, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(_c.error.value), - const SizedBox(height: 20), - PlatformButton( - child: Text('common.retry'.i18n), - onPressed: () { - _c.getContent(); - }, - ) - ], - ), - ); - } - - if (_c.watchData.value == null) { - return const Center(child: ProgressRing()); - } - - final watchData = _c.watchData.value!; - - final listviewPadding = - maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; - - final fontSize = _c.fontSize.value; - - return Center( - child: ScrollablePositionedList.builder( - itemPositionsListener: _c.itemPositionsListener, - initialScrollIndex: _c.positions.value, - itemScrollController: _c.itemScrollController, - scrollOffsetController: _c.scrollOffsetController, - padding: EdgeInsets.symmetric( - horizontal: listviewPadding, - vertical: 16, - ), - itemBuilder: (context, index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text( - _c.title + _c.playList[_c.playIndex].name, - style: const TextStyle(fontSize: 26), - ), - ); - } - if (index == 1) { - return (watchData.subtitle != null) - ? Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text( - watchData.subtitle!, - style: const TextStyle(fontSize: 20), - ), - ) - : const SizedBox(); - } - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: SelectableText.rich( - TextSpan( - children: [ - const WidgetSpan(child: SizedBox(width: 40.0)), - TextSpan( - text: watchData.content[index - 2], - style: TextStyle( - fontSize: fontSize, - fontWeight: FontWeight.w400, - height: 2, - textBaseline: TextBaseline.ideographic, - fontFamily: 'Microsoft Yahei', - ), - ), - ], - ), + return GestureDetector( + onTapDown: (detail) { + _c.showControlPanel(); + }, + child: LayoutBuilder( + builder: (context, constraints) => Obx( + () { + // // 宽度 大于 800 就是整体宽度的一半 + final maxWidth = constraints.maxWidth; + // final width = maxWidth > 800 ? maxWidth / 2 : maxWidth; + // final height = constraints.maxHeight; + if (_c.error.value.isNotEmpty) { + return SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_c.error.value), + const SizedBox(height: 20), + PlatformButton( + child: Text('common.retry'.i18n), + onPressed: () { + _c.getContent(); + }, + ) + ], ), ); - }, - itemCount: watchData.content.length + 2, - ), - ); - - // const TextStyle textStyle = TextStyle( - // fontSize: 18, - // fontWeight: FontWeight.w400, - // height: 2, - // textBaseline: TextBaseline.ideographic, - // ); - - // // 获取每句子的高 - // final List heightList = []; - // for (final String sentence in content) { - // final TextPainter painter = TextPainter( - // text: TextSpan( - // text: sentence, - // style: textStyle, - // ), - // textDirection: TextDirection.ltr, - // )..layout(maxWidth: width - 140); - // heightList.add(painter.height); - // } - - // // 通过高度判断每页能放多少句子 - // final List pageSentenceCount = []; - // double pageHeight = 0; - // int sentenceCount = 0; - // for (final double textHeight in heightList) { - // pageHeight += textHeight; - // sentenceCount++; - // if (pageHeight > height) { - // pageSentenceCount.add(sentenceCount); - // pageHeight = 0; - // sentenceCount = 0; - // } - // } - - // final List pageViewList = []; - - // int pageStartIndex = 0; - // for (final int sentenceCount in pageSentenceCount) { - // final List pageContent = content.sublist( - // pageStartIndex, - // pageStartIndex + sentenceCount, - // ); - // pageStartIndex += sentenceCount; - // pageViewList.add( - // ListView.builder( - // shrinkWrap: true, - // physics: const NeverScrollableScrollPhysics(), - // itemBuilder: (context, index) { - // return Text( - // pageContent[index], - // style: textStyle, - // ); - // }, - // itemCount: pageContent.length, - // ), - // ); - // } - - // return PageView( - // children: [ - // // 如果大于 800 就是整体宽度的一半 - // for (var i = 0; - // i < pageViewList.length; - // maxWidth > 800 ? i += 2 : i++) - // Row( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Expanded( - // child: Container( - // child: pageViewList[i], - // ), - // ), - // if (maxWidth > 800) - // i + 1 < pageViewList.length - // ? Expanded( - // child: Container( - // child: pageViewList[i + 1], - // ), - // ) - // : const Expanded(child: SizedBox()), - // ], - // ) - // ], - // ); - }, - ), - ); + } + + if (_c.watchData.value == null) { + return const Center(child: ProgressRing()); + } + + final watchData = _c.watchData.value!; + + final listviewPadding = + maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; + + final fontSize = _c.fontSize.value; + + return Center( + child: ScrollablePositionedList.builder( + itemPositionsListener: _c.itemPositionsListener, + initialScrollIndex: _c.positions.value, + itemScrollController: _c.itemScrollController, + scrollOffsetController: _c.scrollOffsetController, + padding: EdgeInsets.symmetric( + horizontal: listviewPadding, + vertical: 16, + ), + itemBuilder: (context, index) { + if (index == 0) { + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text( + _c.title + _c.playList[_c.playIndex].name, + style: const TextStyle(fontSize: 26), + ), + ); + } + if (index == 1) { + return (watchData.subtitle != null) + ? Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text( + watchData.subtitle!, + style: const TextStyle(fontSize: 20), + ), + ) + : const SizedBox(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: SelectableText.rich( + TextSpan( + children: [ + const WidgetSpan(child: SizedBox(width: 40.0)), + TextSpan( + text: watchData.content[index - 2], + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w400, + height: 2, + textBaseline: TextBaseline.ideographic, + fontFamily: 'Microsoft Yahei', + ), + ), + ], + ), + ), + ); + }, + itemCount: watchData.content.length + 2, + ), + ); + + // const TextStyle textStyle = TextStyle( + // fontSize: 18, + // fontWeight: FontWeight.w400, + // height: 2, + // textBaseline: TextBaseline.ideographic, + // ); + + // // 获取每句子的高 + // final List heightList = []; + // for (final String sentence in content) { + // final TextPainter painter = TextPainter( + // text: TextSpan( + // text: sentence, + // style: textStyle, + // ), + // textDirection: TextDirection.ltr, + // )..layout(maxWidth: width - 140); + // heightList.add(painter.height); + // } + + // // 通过高度判断每页能放多少句子 + // final List pageSentenceCount = []; + // double pageHeight = 0; + // int sentenceCount = 0; + // for (final double textHeight in heightList) { + // pageHeight += textHeight; + // sentenceCount++; + // if (pageHeight > height) { + // pageSentenceCount.add(sentenceCount); + // pageHeight = 0; + // sentenceCount = 0; + // } + // } + + // final List pageViewList = []; + + // int pageStartIndex = 0; + // for (final int sentenceCount in pageSentenceCount) { + // final List pageContent = content.sublist( + // pageStartIndex, + // pageStartIndex + sentenceCount, + // ); + // pageStartIndex += sentenceCount; + // pageViewList.add( + // ListView.builder( + // shrinkWrap: true, + // physics: const NeverScrollableScrollPhysics(), + // itemBuilder: (context, index) { + // return Text( + // pageContent[index], + // style: textStyle, + // ); + // }, + // itemCount: pageContent.length, + // ), + // ); + // } + + // return PageView( + // children: [ + // // 如果大于 800 就是整体宽度的一半 + // for (var i = 0; + // i < pageViewList.length; + // maxWidth > 800 ? i += 2 : i++) + // Row( + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // Expanded( + // child: Container( + // child: pageViewList[i], + // ), + // ), + // if (maxWidth > 800) + // i + 1 < pageViewList.length + // ? Expanded( + // child: Container( + // child: pageViewList[i + 1], + // ), + // ) + // : const Expanded(child: SizedBox()), + // ], + // ) + // ], + // ); + }, + ), + )); } Widget _buildAndroid(BuildContext context) { diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index c67669b3..6095291a 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:flutter_animate/flutter_animate.dart'; import 'package:get/get.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; import 'package:miru_app/controllers/watch/novel_controller.dart'; @@ -35,68 +34,81 @@ class ControlPanelFooter extends StatelessWidget { ? Theme.of(context).colorScheme.background.withOpacity(0.9) : Colors.transparent; - return SizedBox( - height: 110, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 15), - child: Obx(() => Row(children: [ - const SizedBox( - height: 10, - ), - if (c.index.value > 0) - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50), - color: containerColor, - ), - child: IconButton( - onPressed: () { - c.index.value--; - }, - icon: const Icon(Icons.skip_previous_rounded))), - const Spacer(), - SizedBox( - height: 50, - width: width * 2 / 3, - child: Material( - color: containerColor, - borderRadius: BorderRadius.circular(30), - child: Obx(() { - if (totalObs.value != 0 || - !c.isShowControlPanel.value) { - return Slider( - label: (progressObs.value + 1).toString(), - max: (totalObs.value - 1).toDouble(), - min: 0, - divisions: totalObs.value - 1, - value: progressObs.value.toDouble(), - onChanged: c.isShowControlPanel.value - ? (val) { - c.updateSlider.value = true; - c.progress.value = val.toInt(); - } - : null, - ); - } - return const Slider(value: 0, onChanged: null); - }))), - const Spacer(), - if (c.index.value != c.playList.length - 1) - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50), - color: containerColor, - ), - child: IconButton( - onPressed: () { - c.index.value++; - }, - icon: const Icon(Icons.skip_next_rounded))) - ]))).animate().slideY(begin: 1, end: 0), - ); + return Align( + alignment: const Alignment(0, 1), + child: TweenAnimationBuilder( + builder: (context, value, child) => FractionalTranslation( + translation: value, + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 30), + child: Obx(() => Row(children: [ + const SizedBox( + height: 10, + ), + if (c.index.value > 0) + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: containerColor, + ), + child: IconButton( + onPressed: () { + c.index.value--; + }, + icon: + const Icon(Icons.skip_previous_rounded))), + const Spacer(), + SizedBox( + height: 50, + width: width * 2 / 3, + child: Material( + color: containerColor, + borderRadius: BorderRadius.circular(30), + child: Obx(() { + if (totalObs.value != 0 || + !c.isShowControlPanel.value) { + return Slider( + label: (progressObs.value + 1).toString(), + max: (totalObs.value - 1) < 0 + ? 1 + : (totalObs.value - 1).toDouble(), + min: 0, + divisions: (totalObs.value - 1) < 0 + ? 1 + : totalObs.value - 1, + value: progressObs.value.toDouble(), + onChanged: c.isShowControlPanel.value + ? (val) { + c.updateSlider.value = true; + c.progress.value = val.toInt(); + } + : null, + ); + } + return const Slider( + value: 0, onChanged: null); + }))), + const Spacer(), + if (c.index.value != c.playList.length - 1) + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: containerColor, + ), + child: IconButton( + onPressed: () { + c.index.value++; + }, + icon: const Icon(Icons.skip_next_rounded))) + ])))), + duration: const Duration(milliseconds: 200), + tween: Tween( + begin: (c.isShowControlPanel.value) ? Offset(0, 1) : Offset.zero, + end: (c.isShowControlPanel.value) ? Offset.zero : Offset(0, 1.0)), + )); } } diff --git a/lib/views/widgets/watch/reader_view.dart b/lib/views/widgets/watch/reader_view.dart index df116471..e4573233 100644 --- a/lib/views/widgets/watch/reader_view.dart +++ b/lib/views/widgets/watch/reader_view.dart @@ -92,13 +92,8 @@ class ReaderView extends StatelessWidget { ), ), // 底部控制 - Positioned( - right: 0, - left: 0, - bottom: 0, - child: ControlPanelFooter(tag), - ), ], + ControlPanelFooter(tag), if (c.enableAutoScroll.value) ElevatedButton( onPressed: () { From d5d5754f7c84aab91bae76ddea5a8cecfc6605df Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sat, 27 Jan 2024 13:36:22 +0800 Subject: [PATCH 07/24] some issue need to be fixed --- lib/controllers/watch/comic_controller.dart | 1 + lib/controllers/watch/novel_controller.dart | 1 + lib/controllers/watch/reader_controller.dart | 37 ++- .../reader/comic/comic_reader_content.dart | 20 +- .../reader/novel/novel_reader_content.dart | 2 +- .../widgets/watch/control_panel_footer.dart | 53 +++-- .../widgets/watch/control_panel_header.dart | 218 +++++++++++++----- .../widgets/watch/desktop_command_bar.dart | 18 ++ lib/views/widgets/watch/reader_view.dart | 7 +- 9 files changed, 266 insertions(+), 91 deletions(-) create mode 100644 lib/views/widgets/watch/desktop_command_bar.dart diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 85b4461f..105fe3ab 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -237,6 +237,7 @@ class ComicController extends ReaderController { ); } _barreryTimer!.cancel(); + mouseTimer?.cancel(); WakelockPlus.disable(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); super.onClose(); diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index 814f65c2..74d2aa24 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -118,6 +118,7 @@ class NovelController extends ReaderController { ); } SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + mouseTimer?.cancel(); super.onClose(); } } diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index 7e3f21d5..7e4cb9b8 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -58,6 +58,9 @@ class ReaderController extends GetxController { final RxDouble prevPageHitBox = 0.3.obs; final enableAutoScroll = false.obs; final height = 1000.0.obs; + final RxBool isMouseHover = false.obs; + final RxBool setControllPanel = false.obs; + Timer? mouseTimer; @override void onInit() { getContent(); @@ -82,6 +85,15 @@ class ReaderController extends GetxController { } autoScrollTimer?.cancel(); }); + mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { + debugPrint(setControllPanel.toString()); + if (setControllPanel.value) { + isShowControlPanel.value = true; + return; + } + isShowControlPanel.value = false; + setControllPanel.value = false; + }); super.onInit(); } @@ -99,20 +111,21 @@ class ReaderController extends GetxController { void nextPage() {} - showControlPanel() { - if (isShowControlPanel.value) { - hideControlPanel(); - return; - } - isShowControlPanel.value = true; - _timer?.cancel(); - _timer = Timer(const Duration(seconds: 3), () { - isShowControlPanel.value = false; - }); - } + // showControlPanel() { + // if (isShowControlPanel.value) { + // hideControlPanel(); + // return; + // } + // debugPrint(isMouseHover.toString()); + // isShowControlPanel.value = true; + // _timer?.cancel(); + // _timer = Timer(const Duration(seconds: 3), () { + // isShowControlPanel.value = false; + // }); + // } hideControlPanel() { - isShowControlPanel.value = false; + setControllPanel.value = false; } addHistory(String progress, String totalProgress) async { diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 2cd4075b..b38a15fb 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -217,7 +218,24 @@ class _ComicReaderContentState extends State { Widget imageBuilder(String url) { return GestureDetector( onTapDown: (deatils) { - _c.showControlPanel(); + double counter = 0; + debugPrint( + "${deatils.globalPosition.dx} ${deatils.globalPosition.dy}"); + if (_c.isShowControlPanel.value) { + // _c.setControllPanel.value = false; + // _c.isShowControlPanel.value = false; + Timer.periodic(const Duration(milliseconds: 50), (timer) { + if (counter == 10) { + timer.cancel(); + } + _c.isShowControlPanel.value = false; + counter++; + }); + return; + } + // _c.setControllPanel.value = !_c.isShowControlPanel.value; + _c.setControllPanel.value = true; + // _c.isShowControlPanel.value = true; }, onDoubleTapDown: (details) { showModalBottomSheet( diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index e2e9f374..7ebfe29e 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -22,7 +22,7 @@ class _NovelReaderContentState extends State { _buildContent() { return GestureDetector( onTapDown: (detail) { - _c.showControlPanel(); + _c.setControllPanel.value = !_c.setControllPanel.value; }, child: LayoutBuilder( builder: (context, constraints) => Obx( diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index 6095291a..ebafd065 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -4,18 +4,29 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; import 'package:miru_app/controllers/watch/novel_controller.dart'; +import 'package:miru_app/views/widgets/platform_widget.dart'; -class ControlPanelFooter extends StatelessWidget { +class ControlPanelFooter extends StatefulWidget { const ControlPanelFooter(this.tag, {super.key}); final String tag; @override - Widget build(BuildContext context) { - final c = Get.find(tag: tag); - final int total = (T == NovelController) - ? c.watchData.value?.content.length ?? 0 - : c.watchData.value?.urls.length ?? 0; - final totalObs = total.obs; - final progressObs = c.progress.value.obs; + State createState() => _ControlPanelFooterState(); +} + +class _ControlPanelFooterState + extends State { + late final c = Get.find(tag: widget.tag); + late final int total = (T == NovelController) + ? c.watchData.value?.content.length ?? 0 + : c.watchData.value?.urls.length ?? 0; + late final totalObs = total.obs; + late final progressObs = c.progress.value.obs; + late final Color containerColor = Platform.isAndroid + ? Theme.of(context).colorScheme.background.withOpacity(0.9) + : Colors.transparent; + @override + void initState() { + super.initState(); ever(c.watchData, (callback) { progressObs.value = 0; totalObs.value = (T == NovelController) @@ -29,10 +40,10 @@ class ControlPanelFooter extends StatelessWidget { debugPrint("scrolled ${c.progress.value}"); progressObs.value = c.progress.value; }); + } + + Widget _buildAndroid(BuildContext context) { final double width = MediaQuery.of(context).size.width; - final Color containerColor = Platform.isAndroid - ? Theme.of(context).colorScheme.background.withOpacity(0.9) - : Colors.transparent; return Align( alignment: const Alignment(0, 1), @@ -107,8 +118,24 @@ class ControlPanelFooter extends StatelessWidget { ])))), duration: const Duration(milliseconds: 200), tween: Tween( - begin: (c.isShowControlPanel.value) ? Offset(0, 1) : Offset.zero, - end: (c.isShowControlPanel.value) ? Offset.zero : Offset(0, 1.0)), + begin: (c.isShowControlPanel.value) + ? const Offset(0, 1) + : Offset.zero, + end: (c.isShowControlPanel.value) + ? Offset.zero + : const Offset(0, 1.0)), )); } + + Widget _buildDesktop(BuildContext context) { + return Center(); + } + + @override + Widget build(BuildContext context) { + return PlatformBuildWidget( + androidBuilder: _buildAndroid, + desktopBuilder: _buildAndroid, + ); + } } diff --git a/lib/views/widgets/watch/control_panel_header.dart b/lib/views/widgets/watch/control_panel_header.dart index c27230c9..9ddcb198 100644 --- a/lib/views/widgets/watch/control_panel_header.dart +++ b/lib/views/widgets/watch/control_panel_header.dart @@ -81,72 +81,166 @@ class _ControlPanelHeaderState } Widget _buildDesktop(BuildContext context) { - return Obx( - () => Container( - width: double.infinity, - height: 40, - color: fluent.FluentTheme.of(context).micaBackgroundColor, - padding: const EdgeInsets.only(left: 16), - child: DragToMoveArea( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - fluent.IconButton( - icon: const Icon(fluent.FluentIcons.back), - onPressed: () { - RouterUtils.pop(); - }, - ), - const SizedBox(width: 16), - Text(_c.title + _c.playList[_c.index.value].name), - const Spacer(), - fluent.FlyoutTarget( - controller: _settingFlayoutcontroller, - child: fluent.IconButton( - icon: const Icon(fluent.FluentIcons.settings), - onPressed: () { - _settingFlayoutcontroller.showFlyout(builder: (context) { - return widget.buildSettings(context); - }); - }, + final route = ModalRoute.of(context)?.settings.name; + debugPrint(route ?? ''); + return MouseRegion( + onHover: (details) { + // _c.setControllPanel.value = true; + }, + child: Obx( + () => fluent.Column(children: [ + Container( + width: double.infinity, + height: 40, + color: fluent.FluentTheme.of(context).micaBackgroundColor, + padding: const EdgeInsets.only(left: 16), + child: DragToMoveArea( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + fluent.IconButton( + icon: const Icon(fluent.FluentIcons.back), + onPressed: () { + RouterUtils.pop(); + }, + ), + const SizedBox(width: 16), + Text(_c.title + _c.playList[_c.index.value].name), + const Spacer(), + fluent.FlyoutTarget( + controller: _settingFlayoutcontroller, + child: fluent.IconButton( + icon: const Icon(fluent.FluentIcons.settings), + onPressed: () { + _settingFlayoutcontroller.showFlyout( + builder: (context) { + return widget.buildSettings(context); + }); + }, + ), + ), + const SizedBox(width: 8), + fluent.FlyoutTarget( + controller: _playListFlayoutcontroller, + child: fluent.IconButton( + icon: const Icon(fluent.FluentIcons.collapse_menu), + onPressed: () { + _playListFlayoutcontroller.showFlyout( + builder: (context) { + return SizedBox( + width: 300, + child: Obx( + () => PlayList( + title: _c.title, + list: _c.playList.map((e) => e.name).toList(), + selectIndex: _c.index.value, + onChange: (value) { + _c.index.value = value; + router.pop(); + }, + ), + ), + ); + }); + }, + ), + ), + SizedBox( + width: 138, + child: WindowCaption( + backgroundColor: Colors.transparent, + brightness: fluent.FluentTheme.of(context).brightness, + ), + ) + ], ), ), - const SizedBox(width: 8), - fluent.FlyoutTarget( - controller: _playListFlayoutcontroller, - child: fluent.IconButton( - icon: const Icon(fluent.FluentIcons.collapse_menu), - onPressed: () { - _playListFlayoutcontroller.showFlyout(builder: (context) { - return SizedBox( - width: 300, - child: Obx( - () => PlayList( - title: _c.title, - list: _c.playList.map((e) => e.name).toList(), - selectIndex: _c.index.value, - onChange: (value) { - _c.index.value = value; - router.pop(); - }, + ), + fluent.Container( + height: 40, + color: fluent.FluentTheme.of(context).micaBackgroundColor, + // child: Row( + // children: [ + // commandBaruilder(fluent.IconButton( + // icon: const Icon( + // fluent.FluentIcons.chevron_left, + // // size: 30, + // ), + // onPressed: () {}, + // )) + // ], + // ) + child: Obx(() => fluent.CommandBar( + primaryItems: [ + fluent.CommandBarButton( + icon: const Icon(fluent.FluentIcons.add), + label: SizedBox( + width: 100, + child: fluent.NumberBox( + mode: fluent.SpinButtonPlacementMode.none, + value: _c.progress.value + 1, + onChanged: (value) { + if (value != null) { + _c.progress.value = value - 1; + } + }, + )), + onPressed: null, + ), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => Tooltip( + message: "Create something new!", + child: w, + ), + wrappedItem: fluent.CommandBarButton( + icon: const Icon(fluent.FluentIcons.add), + label: const Text('New'), + onPressed: () {}, ), ), - ); - }); - }, - ), - ), - SizedBox( - width: 138, - child: WindowCaption( - backgroundColor: Colors.transparent, - brightness: fluent.FluentTheme.of(context).brightness, - ), - ) - ], - ), - ), - ).animate().fade(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => Tooltip( + message: "Delete what is currently selected!", + child: w, + ), + wrappedItem: fluent.CommandBarButton( + icon: const Icon(fluent.FluentIcons.delete), + label: const Text('Delete'), + onPressed: () {}, + ), + ), + fluent.CommandBarButton( + icon: const Icon(fluent.FluentIcons.archive), + label: const Text('Archive'), + onPressed: () {}, + ), + fluent.CommandBarButton( + icon: const Icon(fluent.FluentIcons.move), + label: const Text('Move'), + onPressed: () {}, + ), + fluent.CommandBarBuilderItem( + builder: (context, displayMode, widget) => + fluent.NumberBox( + value: 1, + onChanged: null, + ), + wrappedItem: fluent.CommandBarButton( + icon: const Icon(fluent.FluentIcons.add), + label: const Text('New'), + onPressed: () {}, + ), + ), + ], + ))) + ]).animate().fade(), + )); + } + + Widget commandBaruilder(child) { + return Padding( + padding: const EdgeInsets.all(4), + child: child, ); } diff --git a/lib/views/widgets/watch/desktop_command_bar.dart b/lib/views/widgets/watch/desktop_command_bar.dart new file mode 100644 index 00000000..18bc79db --- /dev/null +++ b/lib/views/widgets/watch/desktop_command_bar.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +class DesktopCommandBar extends StatelessWidget { + const DesktopCommandBar( + {super.key, required this.text, this.icon, required this.onPressed}); + final Widget text; + final Widget? icon; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(4), + child: Row( + children: [], + )); + } +} diff --git a/lib/views/widgets/watch/reader_view.dart b/lib/views/widgets/watch/reader_view.dart index e4573233..a2731e02 100644 --- a/lib/views/widgets/watch/reader_view.dart +++ b/lib/views/widgets/watch/reader_view.dart @@ -26,11 +26,14 @@ class ReaderView extends StatelessWidget { MouseRegion( onHover: (event) { if (event.position.dy < 60) { - c.showControlPanel(); + c.setControllPanel.value = true; + return; } if (event.position.dy > LayoutUtils.height - 60) { - c.showControlPanel(); + c.setControllPanel.value = true; + return; } + c.setControllPanel.value = false; }, child: content, ), From 76170f7d310331e75c9e8014c31bc2132cd709e9 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 28 Jan 2024 17:29:31 +0800 Subject: [PATCH 08/24] update desktop manga reader --- lib/controllers/watch/comic_controller.dart | 7 +- lib/controllers/watch/reader_controller.dart | 20 +- .../reader/comic/comic_reader_content.dart | 20 +- .../reader/comic/comic_reader_settings.dart | 224 +++++++++++++----- .../widgets/watch/control_panel_footer.dart | 211 +++++++++++++++-- .../widgets/watch/control_panel_header.dart | 134 +++-------- lib/views/widgets/watch/reader_view.dart | 25 +- 7 files changed, 404 insertions(+), 237 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 105fe3ab..67d7e527 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -31,7 +31,7 @@ class ComicController extends ReaderController { 'webTonn': MangaReadMode.webTonn, }; final String setting = MiruStorage.getSetting(SettingKey.readingMode); - final readType = MangaReadMode.standard.obs; + // final readType = MangaReadMode.standard.obs; final currentScale = 1.0.obs; // 当前页码 final currentPage = 0.obs; @@ -41,6 +41,7 @@ class ComicController extends ReaderController { // 是否已经恢复上次阅读 final isRecover = false.obs; final batteryLevel = 100.obs; + final readType = MangaReadMode.standard.obs; Timer? _barreryTimer; final statusBarElement = { 'reader-settings.battery'.i18n: true.obs, @@ -63,8 +64,8 @@ class ComicController extends ReaderController { void onInit() async { _initSetting(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - WakelockPlus.toggle( - enable: MiruStorage.getSetting(SettingKey.enableWakelock)); + enableWakeLock.value = MiruStorage.getSetting(SettingKey.enableWakelock); + WakelockPlus.toggle(enable: enableWakeLock.value); await _statusBar(); _barreryTimer = Timer.periodic(const Duration(seconds: 10), (timer) => _statusBar()); diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index 7e4cb9b8..f796ee35 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -1,11 +1,10 @@ import 'dart:async'; import 'package:get/get.dart'; import 'package:flutter/material.dart'; -import 'package:miru_app/models/extension.dart'; -import 'package:miru_app/models/history.dart'; import 'package:miru_app/controllers/home_controller.dart'; import 'package:miru_app/data/services/database_service.dart'; import 'package:miru_app/data/services/extension_service.dart'; +import 'package:miru_app/models/index.dart'; import 'package:miru_app/utils/miru_storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -38,7 +37,6 @@ class ReaderController extends GetxController { late final index = playIndex.obs; late final progress = 0.obs; get cuurentPlayUrl => playList[index.value].url; - Timer? _timer; Timer? autoScrollTimer; final isScrolled = true.obs; final updateSlider = true.obs; @@ -61,6 +59,8 @@ class ReaderController extends GetxController { final RxBool isMouseHover = false.obs; final RxBool setControllPanel = false.obs; Timer? mouseTimer; + final RxBool enableWakeLock = false.obs; + // final readType = MangaReadMode.standard.obs; @override void onInit() { getContent(); @@ -92,7 +92,6 @@ class ReaderController extends GetxController { return; } isShowControlPanel.value = false; - setControllPanel.value = false; }); super.onInit(); } @@ -111,19 +110,6 @@ class ReaderController extends GetxController { void nextPage() {} - // showControlPanel() { - // if (isShowControlPanel.value) { - // hideControlPanel(); - // return; - // } - // debugPrint(isMouseHover.toString()); - // isShowControlPanel.value = true; - // _timer?.cancel(); - // _timer = Timer(const Duration(seconds: 3), () { - // isShowControlPanel.value = false; - // }); - // } - hideControlPanel() { setControllPanel.value = false; } diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index b38a15fb..7ec52901 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -1,4 +1,3 @@ -import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -218,24 +217,7 @@ class _ComicReaderContentState extends State { Widget imageBuilder(String url) { return GestureDetector( onTapDown: (deatils) { - double counter = 0; - debugPrint( - "${deatils.globalPosition.dx} ${deatils.globalPosition.dy}"); - if (_c.isShowControlPanel.value) { - // _c.setControllPanel.value = false; - // _c.isShowControlPanel.value = false; - Timer.periodic(const Duration(milliseconds: 50), (timer) { - if (counter == 10) { - timer.cancel(); - } - _c.isShowControlPanel.value = false; - counter++; - }); - return; - } - // _c.setControllPanel.value = !_c.isShowControlPanel.value; - _c.setControllPanel.value = true; - // _c.isShowControlPanel.value = true; + _c.setControllPanel.value = !_c.setControllPanel.value; }, onDoubleTapDown: (details) { showModalBottomSheet( diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index e623ccb1..a6709364 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -8,7 +8,6 @@ import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/utils/miru_storage.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/settings/settings_switch_tile.dart'; -import 'package:miru_app/views/widgets/settings/settings_tile.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; class ComicReaderSettings extends StatefulWidget { @@ -21,7 +20,7 @@ class ComicReaderSettings extends StatefulWidget { class _ComicReaderSettingsState extends State { late final ComicController _c = Get.find(tag: widget.tag); - + final fluent.FlyoutController _readModeFlyout = fluent.FlyoutController(); // double nextPageHitBox = MiruStorage.getSetting(SettingKey.nextPageHitBox); // double prevPageHitBox = MiruStorage.getSetting(SettingKey.prevPageHitBox); Widget _buildAndroid(BuildContext context) { @@ -181,16 +180,16 @@ class _ComicReaderSettingsState extends State { MiruStorage.setSetting( SettingKey.prevPageHitBox, val); }), - SettingsSwitchTile( + Obx(() => SettingsSwitchTile( icon: const Icon(Icons.coffee), title: "reader-settings.enable-wakelock".i18n, - buildValue: () => MiruStorage.getSetting( - SettingKey.enableWakelock), + buildValue: () => _c.enableWakeLock.value, onChanged: (val) { WakelockPlus.toggle(enable: val); + _c.enableWakeLock.value = val; MiruStorage.setSetting( SettingKey.enableWakelock, val); - }), + })), ], )), ), @@ -242,59 +241,102 @@ class _ComicReaderSettingsState extends State { } Widget _buildDesktop(BuildContext context) { - return Obx(() { - return fluent.Card( - backgroundColor: fluent.FluentTheme.of(context).micaBackgroundColor, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('comic-settings.read-mode'.i18n), - const SizedBox(height: 16), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - fluent.ToggleButton( - checked: _c.readType.value == MangaReadMode.standard, - onChanged: (value) { - if (value) { - setState(() { - _c.readType.value = MangaReadMode.standard; - }); - } - }, - child: Text('comic-settings.standard'.i18n), - ), - const SizedBox(width: 8), - fluent.ToggleButton( - checked: _c.readType.value == MangaReadMode.rightToLeft, - onChanged: (value) { - if (value) { - setState(() { - _c.readType.value = MangaReadMode.rightToLeft; - }); - } + return Obx(() => fluent.CommandBar( + primaryItems: [ + CommandBarFlyOutTarget( + controller: _readModeFlyout, + child: fluent.IconButton( + icon: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon( + fluent.FluentIcons.reading_mode, + size: 17, + ), + const SizedBox(width: 8), + Text("comic-settings.read-mode".i18n) + ]), + onPressed: () { + _readModeFlyout.showFlyout( + builder: (context) => fluent.MenuFlyout( + items: [ + fluent.MenuFlyoutItem( + leading: _c.readType.value == + MangaReadMode.standard + ? const Icon( + fluent.FluentIcons.location_dot) + : null, + text: Text("comic-settings.standard".i18n), + onPressed: () { + _c.readType.value = + MangaReadMode.standard; + }), + fluent.MenuFlyoutItem( + leading: _c.readType.value == + MangaReadMode.rightToLeft + ? const Icon( + fluent.FluentIcons.location_dot) + : null, + text: Text( + "comic-settings.right-to-left".i18n), + onPressed: () { + _c.readType.value = + MangaReadMode.rightToLeft; + }), + fluent.MenuFlyoutItem( + leading: _c.readType.value == + MangaReadMode.webTonn + ? const Icon( + fluent.FluentIcons.location_dot) + : null, + text: Text("comic-settings.web-tonn".i18n), + onPressed: () { + _c.readType.value = MangaReadMode.webTonn; + }) + ], + )); }, - child: Text('comic-settings.right-to-left'.i18n), + )), + fluent.CommandBarBuilderItem( + wrappedItem: fluent.CommandBarButton( + label: SizedBox( + width: 40, + child: fluent.NumberBox( + max: _c.watchData.value?.urls.length ?? 1, + min: 1, + mode: fluent.SpinButtonPlacementMode.none, + clearButton: false, + value: _c.progress.value + 1, + onChanged: (value) { + if (value != null) { + _c.updateSlider.value = true; + _c.progress.value = value - 1; + } + }, + )), + onPressed: null, ), - const SizedBox(width: 8), - fluent.ToggleButton( - checked: _c.readType.value == MangaReadMode.webTonn, - onChanged: (value) { - if (value) { - setState(() { - _c.readType.value = MangaReadMode.webTonn; - }); - } + builder: (context, mode, w) => Tooltip( + message: "comic-settings.page".i18n, + child: w, + )), + CommandBarText(text: "/ ${_c.watchData.value?.urls.length ?? 0}"), + const fluent.CommandBarSeparator(thickness: 3), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => Tooltip( + message: "reader-settings.enable-wakelock".i18n, + child: w, + ), + wrappedItem: CommandBarToggleButton( + onchange: (val) { + _c.enableWakeLock.value = val; + WakelockPlus.toggle(enable: val); + MiruStorage.setSetting(SettingKey.enableWakelock, val); }, - child: Text('comic-settings.web-tonn'.i18n), - ) - ], - ) + checked: _c.enableWakeLock.value, + child: + const Icon(fluent.FluentIcons.coffee_script, size: 17)), + ), ], - ), - ); - }); + )); } @override @@ -305,3 +347,75 @@ class _ComicReaderSettingsState extends State { ); } } + +class CommandBarDropDownButton extends fluent.CommandBarItem { + const CommandBarDropDownButton( + {super.key, required this.items, this.onPressed, this.icon, this.label}); + final List items; + final VoidCallback? onPressed; + final Widget? icon; + final Widget? label; + + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Row(mainAxisSize: MainAxisSize.min, children: [ + if (icon != null) ...[icon!, const SizedBox(width: 8)], + fluent.DropDownButton(items: items) + ]); + } +} + +class CommandBarFlyOutTarget extends fluent.CommandBarItem { + const CommandBarFlyOutTarget( + {super.key, required this.controller, required this.child, this.label}); + final fluent.FlyoutController controller; + final Widget child; + final Widget? label; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Row(mainAxisSize: MainAxisSize.min, children: [ + if (label != null) ...[ + label!, + const SizedBox( + width: 6.0, + ) + ], + fluent.FlyoutTarget( + controller: controller, + child: child, + ) + ]); + } +} + +class CommandBarText extends fluent.CommandBarItem { + const CommandBarText({super.key, required this.text}); + final String text; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Text(text); + } +} + +class CommandBarToggleButton extends fluent.CommandBarItem { + const CommandBarToggleButton( + {super.key, + required this.onchange, + required this.checked, + required this.child}); + final bool checked; + final void Function(bool)? onchange; + final Widget child; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return fluent.ToggleButton( + checked: checked, + onChanged: onchange, + child: child, + ); + } +} diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index ebafd065..7b5a9387 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -1,10 +1,11 @@ import 'dart:io'; - import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; import 'package:miru_app/controllers/watch/novel_controller.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; +import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:miru_app/utils/miru_storage.dart'; class ControlPanelFooter extends StatefulWidget { const ControlPanelFooter(this.tag, {super.key}); @@ -15,33 +16,36 @@ class ControlPanelFooter extends StatefulWidget { class _ControlPanelFooterState extends State { - late final c = Get.find(tag: widget.tag); + late final _c = Get.find(tag: widget.tag); late final int total = (T == NovelController) - ? c.watchData.value?.content.length ?? 0 - : c.watchData.value?.urls.length ?? 0; + ? _c.watchData.value?.content.length ?? 0 + : _c.watchData.value?.urls.length ?? 0; late final totalObs = total.obs; - late final progressObs = c.progress.value.obs; + late final progressObs = _c.progress.value.obs; late final Color containerColor = Platform.isAndroid ? Theme.of(context).colorScheme.background.withOpacity(0.9) : Colors.transparent; + @override void initState() { super.initState(); - ever(c.watchData, (callback) { + ever(_c.watchData, (callback) { progressObs.value = 0; totalObs.value = (T == NovelController) - ? c.watchData.value?.content.length ?? 0 - : c.watchData.value?.urls.length ?? 0; + ? _c.watchData.value?.content.length ?? 0 + : _c.watchData.value?.urls.length ?? 0; }); - ever(c.progress, (callback) { + ever(_c.progress, (callback) { progressObs.value = callback; }); - ever(c.isShowControlPanel, (callback) { - debugPrint("scrolled ${c.progress.value}"); - progressObs.value = c.progress.value; + ever(_c.isShowControlPanel, (callback) { + // debugPrint("scrolled ${c.progress.value}"); + progressObs.value = _c.progress.value; }); } + final _desktopOffsetFlyoutController = fluent.FlyoutController(); + final _desktopIntervalFlyoutController = fluent.FlyoutController(); Widget _buildAndroid(BuildContext context) { final double width = MediaQuery.of(context).size.width; @@ -56,7 +60,7 @@ class _ControlPanelFooterState const SizedBox( height: 10, ), - if (c.index.value > 0) + if (_c.index.value > 0) Container( width: 40, height: 40, @@ -66,7 +70,7 @@ class _ControlPanelFooterState ), child: IconButton( onPressed: () { - c.index.value--; + _c.index.value--; }, icon: const Icon(Icons.skip_previous_rounded))), @@ -79,7 +83,7 @@ class _ControlPanelFooterState borderRadius: BorderRadius.circular(30), child: Obx(() { if (totalObs.value != 0 || - !c.isShowControlPanel.value) { + !_c.isShowControlPanel.value) { return Slider( label: (progressObs.value + 1).toString(), max: (totalObs.value - 1) < 0 @@ -90,10 +94,10 @@ class _ControlPanelFooterState ? 1 : totalObs.value - 1, value: progressObs.value.toDouble(), - onChanged: c.isShowControlPanel.value + onChanged: _c.isShowControlPanel.value ? (val) { - c.updateSlider.value = true; - c.progress.value = val.toInt(); + _c.updateSlider.value = true; + _c.progress.value = val.toInt(); } : null, ); @@ -102,7 +106,7 @@ class _ControlPanelFooterState value: 0, onChanged: null); }))), const Spacer(), - if (c.index.value != c.playList.length - 1) + if (_c.index.value != _c.playList.length - 1) Container( width: 40, height: 40, @@ -112,30 +116,189 @@ class _ControlPanelFooterState ), child: IconButton( onPressed: () { - c.index.value++; + _c.index.value++; }, icon: const Icon(Icons.skip_next_rounded))) ])))), duration: const Duration(milliseconds: 200), tween: Tween( - begin: (c.isShowControlPanel.value) + begin: (_c.isShowControlPanel.value) ? const Offset(0, 1) : Offset.zero, - end: (c.isShowControlPanel.value) + end: (_c.isShowControlPanel.value) ? Offset.zero : const Offset(0, 1.0)), )); } Widget _buildDesktop(BuildContext context) { - return Center(); + final double width = MediaQuery.of(context).size.width; + final double height = MediaQuery.of(context).size.height; + return Align( + alignment: const Alignment(0, 1), + child: TweenAnimationBuilder( + builder: (context, value, child) => FractionalTranslation( + translation: value, + child: Container( + color: fluent.FluentTheme.of(context) + .micaBackgroundColor + .withOpacity(0.75), + height: 80, + child: Obx(() => Column(children: [ + const SizedBox( + height: 4, + ), + Row(children: [ + const SizedBox(width: 16), + Text((progressObs.value + 1).toString()), + const SizedBox(width: 8), + Obx(() { + if (totalObs.value != 0 || + !_c.isShowControlPanel.value) { + return Expanded( + child: fluent.Slider( + label: (progressObs.value + 1).toString(), + max: (totalObs.value - 1) < 0 + ? 1 + : (totalObs.value - 1).toDouble(), + min: 0, + divisions: (totalObs.value - 1) < 0 + ? 1 + : totalObs.value - 1, + value: progressObs.value.toDouble(), + onChanged: _c.isShowControlPanel.value + ? (val) { + _c.updateSlider.value = true; + _c.progress.value = val.toInt(); + } + : null, + )); + } + return const Expanded( + child: + fluent.Slider(value: 0, onChanged: null)); + }), + const SizedBox(width: 8), + Text(totalObs.value.toString()), + const SizedBox(width: 16), + ]), + Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(width: 48), + fluent.FlyoutTarget( + controller: _desktopIntervalFlyoutController, + child: _desktopMangaPlayerButton( + 20, fluent.FluentIcons.clock, () { + _desktopIntervalFlyoutController.showFlyout( + builder: (context) => Obx(() => + fluent.FlyoutContent( + child: SizedBox( + width: 20, + height: height / 3, + child: fluent.Slider( + vertical: true, + value: _c + .autoScrollInterval + .value + .toDouble(), + max: 500.0, + divisions: 25, + label: + "${_c.autoScrollInterval} ms", + onChanged: (val) { + _c.autoScrollInterval + .value = + val.toInt(); + MiruStorage.setSetting( + SettingKey + .autoScrollInterval, + val.toInt()); + }))))); + })), + const Spacer(flex: 4), + _desktopMangaPlayerButton( + 20, fluent.FluentIcons.previous, () { + _c.index.value--; + }), + const Spacer(), + _desktopMangaPlayerButton( + 40, + (_c.enableAutoScroll.value) + ? fluent.FluentIcons.stop + : fluent.FluentIcons.play, () { + _c.enableAutoScroll.value = + !_c.enableAutoScroll.value; + }), + const Spacer(), + _desktopMangaPlayerButton( + 20, fluent.FluentIcons.next, () { + _c.index.value++; + }), + const Spacer(flex: 4), + fluent.FlyoutTarget( + controller: _desktopOffsetFlyoutController, + child: _desktopMangaPlayerButton( + 20, fluent.FluentIcons.padding, () { + _desktopOffsetFlyoutController.showFlyout( + builder: (context) => Obx(() => + fluent.FlyoutContent( + child: SizedBox( + width: 20, + height: height / 3, + child: fluent.Slider( + vertical: true, + value: _c + .autoScrollOffset + .value, + max: 300.0, + divisions: 30, + label: + "${_c.autoScrollOffset} pixels", + onChanged: (val) { + _c.autoScrollOffset + .value = val; + MiruStorage.setSetting( + SettingKey + .autoScrollOffset, + val); + }))))); + })), + const SizedBox(width: 16), + ]) + ])))), + duration: const Duration(milliseconds: 200), + tween: Tween( + begin: (_c.isShowControlPanel.value || _c.enableAutoScroll.value) + ? const Offset(0, 1) + : Offset.zero, + end: (_c.isShowControlPanel.value || _c.enableAutoScroll.value) + ? Offset.zero + : const Offset(0, 1.0)), + )); + } + + Widget _desktopMangaPlayerButton( + double? size, IconData icon, VoidCallback? onPressed) { + return fluent.IconButton( + style: fluent.ButtonStyle( + shape: fluent.ButtonState.resolveWith((states) => + const fluent.RoundedRectangleBorder( + borderRadius: + fluent.BorderRadius.all(fluent.Radius.circular(50))))), + onPressed: onPressed, + icon: Icon( + icon, + size: size, + ), + ); } @override Widget build(BuildContext context) { return PlatformBuildWidget( androidBuilder: _buildAndroid, - desktopBuilder: _buildAndroid, + desktopBuilder: _buildDesktop, ); } } diff --git a/lib/views/widgets/watch/control_panel_header.dart b/lib/views/widgets/watch/control_panel_header.dart index 9ddcb198..b669855b 100644 --- a/lib/views/widgets/watch/control_panel_header.dart +++ b/lib/views/widgets/watch/control_panel_header.dart @@ -27,8 +27,6 @@ class _ControlPanelHeaderState late final _c = Get.find(tag: widget.tag); final fluent.FlyoutController _playListFlayoutcontroller = fluent.FlyoutController(); - final fluent.FlyoutController _settingFlayoutcontroller = - fluent.FlyoutController(); Widget _buildAndroid(BuildContext context) { return SafeArea( @@ -83,17 +81,17 @@ class _ControlPanelHeaderState Widget _buildDesktop(BuildContext context) { final route = ModalRoute.of(context)?.settings.name; debugPrint(route ?? ''); - return MouseRegion( - onHover: (details) { - // _c.setControllPanel.value = true; - }, - child: Obx( - () => fluent.Column(children: [ - Container( - width: double.infinity, - height: 40, - color: fluent.FluentTheme.of(context).micaBackgroundColor, - padding: const EdgeInsets.only(left: 16), + return Obx( + () => fluent.Column(children: [ + Container( + width: double.infinity, + height: 40, + color: fluent.FluentTheme.of(context).micaBackgroundColor, + padding: const EdgeInsets.only(left: 16), + child: MouseRegion( + onHover: (detail) { + _c.setControllPanel.value = true; + }, child: DragToMoveArea( child: Row( mainAxisSize: MainAxisSize.min, @@ -107,19 +105,7 @@ class _ControlPanelHeaderState const SizedBox(width: 16), Text(_c.title + _c.playList[_c.index.value].name), const Spacer(), - fluent.FlyoutTarget( - controller: _settingFlayoutcontroller, - child: fluent.IconButton( - icon: const Icon(fluent.FluentIcons.settings), - onPressed: () { - _settingFlayoutcontroller.showFlyout( - builder: (context) { - return widget.buildSettings(context); - }); - }, - ), - ), - const SizedBox(width: 8), + // const SizedBox(width: 8), fluent.FlyoutTarget( controller: _playListFlayoutcontroller, child: fluent.IconButton( @@ -154,87 +140,15 @@ class _ControlPanelHeaderState ) ], ), - ), - ), - fluent.Container( - height: 40, - color: fluent.FluentTheme.of(context).micaBackgroundColor, - // child: Row( - // children: [ - // commandBaruilder(fluent.IconButton( - // icon: const Icon( - // fluent.FluentIcons.chevron_left, - // // size: 30, - // ), - // onPressed: () {}, - // )) - // ], - // ) - child: Obx(() => fluent.CommandBar( - primaryItems: [ - fluent.CommandBarButton( - icon: const Icon(fluent.FluentIcons.add), - label: SizedBox( - width: 100, - child: fluent.NumberBox( - mode: fluent.SpinButtonPlacementMode.none, - value: _c.progress.value + 1, - onChanged: (value) { - if (value != null) { - _c.progress.value = value - 1; - } - }, - )), - onPressed: null, - ), - fluent.CommandBarBuilderItem( - builder: (context, mode, w) => Tooltip( - message: "Create something new!", - child: w, - ), - wrappedItem: fluent.CommandBarButton( - icon: const Icon(fluent.FluentIcons.add), - label: const Text('New'), - onPressed: () {}, - ), - ), - fluent.CommandBarBuilderItem( - builder: (context, mode, w) => Tooltip( - message: "Delete what is currently selected!", - child: w, - ), - wrappedItem: fluent.CommandBarButton( - icon: const Icon(fluent.FluentIcons.delete), - label: const Text('Delete'), - onPressed: () {}, - ), - ), - fluent.CommandBarButton( - icon: const Icon(fluent.FluentIcons.archive), - label: const Text('Archive'), - onPressed: () {}, - ), - fluent.CommandBarButton( - icon: const Icon(fluent.FluentIcons.move), - label: const Text('Move'), - onPressed: () {}, - ), - fluent.CommandBarBuilderItem( - builder: (context, displayMode, widget) => - fluent.NumberBox( - value: 1, - onChanged: null, - ), - wrappedItem: fluent.CommandBarButton( - icon: const Icon(fluent.FluentIcons.add), - label: const Text('New'), - onPressed: () {}, - ), - ), - ], - ))) - ]).animate().fade(), - )); + )), + ), + fluent.Container( + height: 40, + color: fluent.FluentTheme.of(context).micaBackgroundColor, + child: widget.buildSettings(context)), + // Obx()) + ]).animate().fade(), + ); } Widget commandBaruilder(child) { @@ -251,4 +165,10 @@ class _ControlPanelHeaderState desktopBuilder: _buildDesktop, ); } + + @override + void dispose() { + _playListFlayoutcontroller.dispose(); + super.dispose(); + } } diff --git a/lib/views/widgets/watch/reader_view.dart b/lib/views/widgets/watch/reader_view.dart index a2731e02..4810bf33 100644 --- a/lib/views/widgets/watch/reader_view.dart +++ b/lib/views/widgets/watch/reader_view.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:miru_app/utils/layout.dart'; @@ -24,17 +26,16 @@ class ReaderView extends StatelessWidget { () => Stack( children: [ MouseRegion( - onHover: (event) { - if (event.position.dy < 60) { - c.setControllPanel.value = true; - return; - } - if (event.position.dy > LayoutUtils.height - 60) { - c.setControllPanel.value = true; - return; - } - c.setControllPanel.value = false; - }, + onHover: (Platform.isAndroid) + ? null + : (event) { + if (event.position.dy < 60 || + event.position.dy > LayoutUtils.height - 60) { + c.setControllPanel.value = true; + return; + } + c.setControllPanel.value = false; + }, child: content, ), @@ -97,7 +98,7 @@ class ReaderView extends StatelessWidget { // 底部控制 ], ControlPanelFooter(tag), - if (c.enableAutoScroll.value) + if (c.enableAutoScroll.value && Platform.isAndroid) ElevatedButton( onPressed: () { c.enableAutoScroll.value = false; From 31fad4d165740ce806a7e85c868ba608c66f2d89 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:28:07 +0800 Subject: [PATCH 09/24] add fullscreen options to desktop reader --- lib/controllers/watch/comic_controller.dart | 10 +- lib/controllers/watch/reader_controller.dart | 1 + .../watch/reader/comic/comic_reader.dart | 6 +- .../reader/comic/comic_reader_content.dart | 176 ++++++++++++------ .../reader/comic/comic_reader_settings.dart | 29 ++- .../widgets/watch/control_panel_footer.dart | 6 +- .../widgets/watch/control_panel_header.dart | 2 +- 7 files changed, 157 insertions(+), 73 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 67d7e527..ae0c1bd4 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -13,6 +15,7 @@ import 'dart:async'; import 'package:battery_plus/battery_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:miru_app/utils/i18n.dart'; +import 'package:window_manager/window_manager.dart'; class ComicController extends ReaderController { ComicController({ @@ -60,6 +63,8 @@ class ComicController extends ReaderController { currentTime.value = "$hour:$minute"; } + final isScrollEnd = false.obs; + @override void onInit() async { _initSetting(); @@ -217,7 +222,7 @@ class ComicController extends ReaderController { } @override - void onClose() { + void onClose() async { if (super.watchData.value != null) { // 获取所有页数量 final pages = super.watchData.value!.urls.length; @@ -241,6 +246,9 @@ class ComicController extends ReaderController { mouseTimer?.cancel(); WakelockPlus.disable(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!Platform.isAndroid) { + await WindowManager.instance.setFullScreen(false); + } super.onClose(); } } diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index f796ee35..a4a9d4de 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -60,6 +60,7 @@ class ReaderController extends GetxController { final RxBool setControllPanel = false.obs; Timer? mouseTimer; final RxBool enableWakeLock = false.obs; + final RxBool enableFullScreen = false.obs; // final readType = MangaReadMode.standard.obs; @override void onInit() { diff --git a/lib/views/pages/watch/reader/comic/comic_reader.dart b/lib/views/pages/watch/reader/comic/comic_reader.dart index 008ad83b..dbb1ba3e 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader.dart @@ -50,21 +50,21 @@ class _ComicReaderState extends State { cover: widget.cover, anilistID: widget.anilistID, ), - tag: widget.title, + tag: widget.playerIndex.toString(), ); super.initState(); } @override void dispose() { - Get.delete(tag: widget.title); + Get.delete(tag: widget.playerIndex.toString()); super.dispose(); } @override Widget build(BuildContext context) { return ReaderView( - widget.title, + widget.playerIndex.toString(), content: PlatformWidget( androidWidget: ComicReaderContent(widget.title), desktopWidget: DragToMoveArea( diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 7ec52901..0cf3001e 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -7,6 +7,7 @@ import 'package:miru_app/controllers/watch/comic_controller.dart'; import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/views/widgets/button.dart'; import 'package:miru_app/views/widgets/cache_network_image.dart'; + import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/progress.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -26,7 +27,8 @@ class _ComicReaderContentState extends State { // 按下数量 final List _pointer = []; - + final menuController = fluent.FlyoutController(); + final contextAttachKey = GlobalKey(); _buildPlaceholder(BuildContext context) { final width = MediaQuery.of(context).size.width; final height = MediaQuery.of(context).size.height; @@ -147,45 +149,74 @@ class _ComicReaderContentState extends State { if (readerType == MangaReadMode.webTonn) { final width = MediaQuery.of(context).size.width; final height = MediaQuery.of(context).size.height; - return SizedBox( - width: width, - height: height, - child: Listener( - onPointerDown: (event) { - _pointer.add(event.pointer); - if (_pointer.length == 2) { - _c.isZoom.value = true; + return NotificationListener( + onNotification: (scrollEnd) { + final metrics = scrollEnd.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels == 0; + if (isTop) { + debugPrint('At the top'); + _c.isScrollEnd.value = false; + } else { + debugPrint('At the bottom'); + _c.isScrollEnd.value = true; + } } + return true; }, - onPointerUp: (event) { - _pointer.remove(event.pointer); - if (_pointer.length == 1) { - _c.isZoom.value = false; - } - }, - child: InteractiveViewer( - scaleEnabled: _c.isZoom.value, - child: ScrollablePositionedList.builder( - physics: _c.isZoom.value - ? const NeverScrollableScrollPhysics() - : null, - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - initialScrollIndex: cuurentPage, - itemScrollController: _c.itemScrollController, - itemPositionsListener: _c.itemPositionsListener, - scrollOffsetController: _c.scrollOffsetController, - scrollOffsetListener: _c.scrollOffsetListener, - itemBuilder: (context, index) { - final url = images[index]; - return imageBuilder(url); - }, - itemCount: images.length, - ), - ), - ), - ); + child: SingleChildScrollView( + physics: (_c.isScrollEnd.value) + ? null + : const NeverScrollableScrollPhysics(), + child: Column(children: [ + SizedBox( + width: width, + height: height, + child: Listener( + onPointerDown: (event) { + _pointer.add(event.pointer); + if (_pointer.length == 2) { + _c.isZoom.value = true; + } + }, + onPointerUp: (event) { + _pointer.remove(event.pointer); + if (_pointer.length == 1) { + _c.isZoom.value = false; + } + }, + child: InteractiveViewer( + scaleEnabled: _c.isZoom.value, + child: ScrollablePositionedList.builder( + physics: + _c.isZoom.value || _c.isScrollEnd.value + ? const NeverScrollableScrollPhysics() + : null, + padding: EdgeInsets.symmetric( + horizontal: viewPadding, + ), + initialScrollIndex: cuurentPage, + itemScrollController: _c.itemScrollController, + itemPositionsListener: + _c.itemPositionsListener, + scrollOffsetController: + _c.scrollOffsetController, + scrollOffsetListener: _c.scrollOffsetListener, + itemBuilder: (context, index) { + final url = images[index]; + return imageBuilder(url); + }, + itemCount: images.length, + ), + ), + ), + ), + Container( + width: width, + height: height, + color: Colors.green, + ) + ]))); } //common mode and left to right mode @@ -219,29 +250,56 @@ class _ComicReaderContentState extends State { onTapDown: (deatils) { _c.setControllPanel.value = !_c.setControllPanel.value; }, - onDoubleTapDown: (details) { - showModalBottomSheet( - context: context, - showDragHandle: true, - useSafeArea: true, - builder: (_) => SizedBox( - height: 100, - child: Column( - children: [ - ListTile( - leading: const Icon(Icons.save), - title: Text('common.save'.i18n), - onTap: () { - Navigator.of(context).pop(); - saveImage( - url, _c.watchData.value?.headers, mounted, context); - }, - ), - ], - ), - ), + onSecondaryTapUp: (d) { + final targetContext = contextAttachKey.currentContext; + if (targetContext == null) return; + final box = targetContext.findRenderObject() as RenderBox; + final position = box.localToGlobal( + d.localPosition, + ancestor: Navigator.of(context).context.findRenderObject(), + ); + menuController.showFlyout( + position: position, + builder: (context) { + return fluent.MenuFlyout(items: [ + fluent.MenuFlyoutItem( + leading: const Icon(fluent.FluentIcons.save), + text: Text('common.save'.i18n), + onPressed: () { + fluent.Flyout.of(context).close(); + saveImage( + url, _c.watchData.value?.headers, mounted, context); + }, + ), + ]); + }, ); }, + onDoubleTapDown: (Platform.isAndroid) + ? (details) { + showModalBottomSheet( + context: context, + showDragHandle: true, + useSafeArea: true, + builder: (_) => SizedBox( + height: 100, + child: Column( + children: [ + ListTile( + leading: const Icon(Icons.save), + title: Text('common.save'.i18n), + onTap: () { + Navigator.of(context).pop(); + saveImage(url, _c.watchData.value?.headers, mounted, + context); + }, + ), + ], + ), + ), + ); + } + : null, child: CacheNetWorkImagePic( url, fit: BoxFit.fitWidth, diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index a6709364..5911c85c 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -9,6 +9,7 @@ import 'package:miru_app/utils/miru_storage.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/settings/settings_switch_tile.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:window_manager/window_manager.dart'; class ComicReaderSettings extends StatefulWidget { const ComicReaderSettings(this.tag, {super.key}); @@ -335,6 +336,20 @@ class _ComicReaderSettingsState extends State { child: const Icon(fluent.FluentIcons.coffee_script, size: 17)), ), + const fluent.CommandBarSeparator(thickness: 3), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => Tooltip( + message: "reader-settings.enable-fullScreen".i18n, + child: w, + ), + wrappedItem: CommandBarToggleButton( + onchange: (val) async { + _c.enableFullScreen.value = val; + await windowManager.setFullScreen(val); + }, + checked: _c.enableFullScreen.value, + child: const Icon(fluent.FluentIcons.full_screen, size: 17)), + ) ], )); } @@ -396,7 +411,7 @@ class CommandBarText extends fluent.CommandBarItem { @override Widget build( BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { - return Text(text); + return Padding(padding: const EdgeInsets.all(10), child: Text(text)); } } @@ -412,10 +427,12 @@ class CommandBarToggleButton extends fluent.CommandBarItem { @override Widget build( BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { - return fluent.ToggleButton( - checked: checked, - onChanged: onchange, - child: child, - ); + return Padding( + padding: const EdgeInsets.all(10), + child: fluent.ToggleButton( + checked: checked, + onChanged: onchange, + child: child, + )); } } diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index 7b5a9387..c556dc59 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -216,7 +216,7 @@ class _ControlPanelFooterState val.toInt()); }))))); })), - const Spacer(flex: 4), + const Spacer(flex: 10), _desktopMangaPlayerButton( 20, fluent.FluentIcons.previous, () { _c.index.value--; @@ -235,7 +235,7 @@ class _ControlPanelFooterState 20, fluent.FluentIcons.next, () { _c.index.value++; }), - const Spacer(flex: 4), + const Spacer(flex: 10), fluent.FlyoutTarget( controller: _desktopOffsetFlyoutController, child: _desktopMangaPlayerButton( @@ -264,7 +264,7 @@ class _ControlPanelFooterState val); }))))); })), - const SizedBox(width: 16), + const SizedBox(width: 48), ]) ])))), duration: const Duration(milliseconds: 200), diff --git a/lib/views/widgets/watch/control_panel_header.dart b/lib/views/widgets/watch/control_panel_header.dart index b669855b..42881cf4 100644 --- a/lib/views/widgets/watch/control_panel_header.dart +++ b/lib/views/widgets/watch/control_panel_header.dart @@ -143,7 +143,7 @@ class _ControlPanelHeaderState )), ), fluent.Container( - height: 40, + height: 70, color: fluent.FluentTheme.of(context).micaBackgroundColor, child: widget.buildSettings(context)), // Obx()) From 99322faa2d56550590183917f04090d52989e7c8 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Tue, 30 Jan 2024 00:48:20 +0800 Subject: [PATCH 10/24] fix error --- lib/views/pages/watch/reader/comic/comic_reader.dart | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/views/pages/watch/reader/comic/comic_reader.dart b/lib/views/pages/watch/reader/comic/comic_reader.dart index dbb1ba3e..9037efee 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader.dart @@ -66,11 +66,12 @@ class _ComicReaderState extends State { return ReaderView( widget.playerIndex.toString(), content: PlatformWidget( - androidWidget: ComicReaderContent(widget.title), + androidWidget: ComicReaderContent(widget.playerIndex.toString()), desktopWidget: DragToMoveArea( - child: ComicReaderContent(widget.title), + child: ComicReaderContent(widget.playerIndex.toString()), )), - buildSettings: (context) => ComicReaderSettings(widget.title), + buildSettings: (context) => + ComicReaderSettings(widget.playerIndex.toString()), ); } } From e4718070b2af377598e27680a68867d4be82ef83 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Wed, 7 Feb 2024 23:08:41 +0800 Subject: [PATCH 11/24] add infinity scroll in comic mode --- lib/controllers/watch/comic_controller.dart | 111 +++++++- lib/controllers/watch/novel_controller.dart | 9 + lib/controllers/watch/reader_controller.dart | 48 +++- .../reader/comic/comic_reader_content.dart | 237 +++++++++++------- .../reader/comic/comic_reader_settings.dart | 4 +- lib/views/widgets/cache_network_image.dart | 4 +- .../widgets/watch/control_panel_footer.dart | 39 +-- .../widgets/watch/desktop_command_bar.dart | 32 +-- 8 files changed, 339 insertions(+), 145 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index ae0c1bd4..ff37e516 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -1,5 +1,4 @@ import 'dart:io'; - import 'package:fluent_ui/fluent_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -35,9 +34,9 @@ class ComicController extends ReaderController { }; final String setting = MiruStorage.getSetting(SettingKey.readingMode); // final readType = MangaReadMode.standard.obs; + final currentScale = 1.0.obs; // 当前页码 - final currentPage = 0.obs; final pageController = ExtendedPageController().obs; final itemPositionsListener = ItemPositionsListener.create(); final alignMode = Alignment.bottomLeft.obs; @@ -45,7 +44,9 @@ class ComicController extends ReaderController { final isRecover = false.obs; final batteryLevel = 100.obs; final readType = MangaReadMode.standard.obs; + final globalScrollController = ScrollController(); Timer? _barreryTimer; + final currentOffset = 0.0.obs; final statusBarElement = { 'reader-settings.battery'.i18n: true.obs, 'reader-settings.time'.i18n: true.obs, @@ -68,6 +69,14 @@ class ComicController extends ReaderController { @override void onInit() async { _initSetting(); + getContent(); + // getTartgetContent(playIndex); + Timer.periodic(const Duration(milliseconds: 500), (timer) { + if (globalItemScrollController.isAttached) { + globalItemScrollController.jumpTo(index: index.value); + timer.cancel(); + } + }); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); enableWakeLock.value = MiruStorage.getSetting(SettingKey.enableWakelock); WakelockPlus.toggle(enable: enableWakeLock.value); @@ -79,7 +88,7 @@ class ComicController extends ReaderController { return; } final pos = itemPositionsListener.itemPositions.value.first; - currentPage.value = pos.index; + currentGlobalProgress.value = pos.index; }); scrollOffsetListener.changes.listen((event) { hideControlPanel(); @@ -88,7 +97,7 @@ class ComicController extends ReaderController { super.height.value = callback; }); ever(readType, (callback) { - _jumpPage(currentPage.value); + _jumpPage(currentGlobalProgress.value); // 保存设置 DatabaseService.setMangaReaderType( super.detailUrl, @@ -96,24 +105,38 @@ class ComicController extends ReaderController { ); }); // 如果切换章节,重置当前页码 - ever(super.index, (callback) => currentPage.value = 0); + // ever(super.index, (callback) => currentPage.value = 0); //control footer 的 slider 改變時,更新頁碼 ever(progress, (callback) { // 防止逆向回饋 if (!updateSlider.value) { return; } - currentPage.value = callback; + currentGlobalProgress.value = callback; _jumpPage(callback); }); - ever(currentPage, (callback) { - progress.value = callback; + ever(currentGlobalProgress, (callback) { + if (updateSlider.value) { + progress.value = callback; + } updateSlider.value = false; + int fullIndex = 0; + debugPrint(currentLocalProgress.value.toString()); + for (int i = 0; i < itemlength.length; i++) { + fullIndex += itemlength[i]; + if (fullIndex > callback) { + index.value = i; + super.index.value = i; + currentLocalProgress.value = callback - (fullIndex - itemlength[i]); + break; + } + } }); ever(super.watchData, (callback) async { if (isRecover.value || callback == null) { return; } + getTartgetContent(playIndex); isRecover.value = true; // 获取上次阅读的页码 final history = await DatabaseService.getHistoryByPackageAndUrl( @@ -127,12 +150,32 @@ class ComicController extends ReaderController { history.episodeId != index.value) { return; } - currentPage.value = int.parse(history.progress); - _jumpPage(currentPage.value); + currentGlobalProgress.value = int.parse(history.progress); + _jumpPage(currentGlobalProgress.value); + // jumpScroller(index.value); }); super.onInit(); } + getTartgetContent(int targetIndex) async { + try { + if (targetIndex < 0 || targetIndex == itemlength.length) { + return; + } + final dynamic updatedData = + await runtime.watch(playList[targetIndex].url); + // if (targetIndex < index.value && items[targetIndex].isEmpty) { + // _jumpPage(itemlength[targetIndex] + currentGlobalProgress.value); + // } + items[targetIndex] = updatedData.urls as List; + itemlength[targetIndex] = updatedData.urls.length; + isScrollEnd.value = false; + // index.value = targetIndex; + } catch (e) { + error.value = e.toString(); + } + } + onKey(RawKeyEvent event) { // 按下 ctrl isZoom.value = event.isControlPressed; @@ -171,6 +214,17 @@ class ComicController extends ReaderController { ); } + jumpScroller(int pos) async { + if (readType.value == MangaReadMode.webTonn) { + if (globalItemScrollController.isAttached) { + globalItemScrollController.jumpTo( + index: pos, + ); + } + return; + } + } + _jumpPage(int page) async { if (readType.value == MangaReadMode.webTonn) { if (itemScrollController.isAttached) { @@ -221,13 +275,32 @@ class ComicController extends ReaderController { } } + @override + Future loadNextChapter() async { + await getTartgetContent(index.value + 1); + return; + } + + Future loadPrevChapter() async { + await getTartgetContent(index.value - 1); + if (itemScrollController.isAttached) { + itemScrollController.scrollTo( + index: itemlength[index.value - 1], + duration: const Duration(milliseconds: 10)); + return; + } + if (pageController.value.hasClients) { + pageController.value.jumpToPage(itemlength[index.value - 1]); + } + } + @override void onClose() async { if (super.watchData.value != null) { // 获取所有页数量 final pages = super.watchData.value!.urls.length; super.addHistory( - currentPage.value.toString(), + currentGlobalProgress.value.toString(), pages.toString(), ); } @@ -251,4 +324,20 @@ class ComicController extends ReaderController { } super.onClose(); } + + @override + void nextChap() { + watchData.value = null; + clearData(); + index.value++; + getContent(); + } + + @override + void prevChap() { + watchData.value = null; + clearData(); + index.value--; + getContent(); + } } diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index 74d2aa24..43c13cc5 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -121,4 +121,13 @@ class NovelController extends ReaderController { mouseTimer?.cancel(); super.onClose(); } + + @override + void nextPage() {} + @override + void previousPage() {} + @override + void nextChap() {} + @override + void prevChap() {} } diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index a4a9d4de..b9b1e9cf 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -8,7 +8,7 @@ import 'package:miru_app/models/index.dart'; import 'package:miru_app/utils/miru_storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -class ReaderController extends GetxController { +abstract class ReaderController extends GetxController { final String title; final List playList; final String detailUrl; @@ -33,6 +33,8 @@ class ReaderController extends GetxController { late Rx watchData = Rx(null); final error = ''.obs; + final globalItemPositionsListener = ItemPositionsListener.create(); + final globalItemScrollController = ItemScrollController(); final isShowControlPanel = false.obs; late final index = playIndex.obs; late final progress = 0.obs; @@ -40,6 +42,8 @@ class ReaderController extends GetxController { Timer? autoScrollTimer; final isScrolled = true.obs; final updateSlider = true.obs; + final isInfinityScrollMode = false.obs; + final isLoading = false.obs; //點擊區域是否反轉 final RxBool tapRegionIsReversed = false.obs; final dynamic _nextPageHitBox = @@ -61,15 +65,22 @@ class ReaderController extends GetxController { Timer? mouseTimer; final RxBool enableWakeLock = false.obs; final RxBool enableFullScreen = false.obs; - // final readType = MangaReadMode.standard.obs; + late final RxList> items = + List.filled(playList.length, []).obs; + late final List itemlength = List.filled(playList.length, 0); + final currentGlobalProgress = 0.obs; + final currentLocalProgress = 0.obs; @override void onInit() { - getContent(); + // getContent(); autoScrollInterval.value = _autoScrollInterval; autoScrollOffset.value = _autoScrollOffset; nextPageHitBox.value = _nextPageHitBox; prevPageHitBox.value = _prevPageHitBox; - ever(index, (callback) => getContent()); + // ever(index, (callback) { + // getContent(); + // }); + ever(enableAutoScroll, (callback) { if (callback) { autoScrollTimer = Timer.periodic( @@ -87,7 +98,6 @@ class ReaderController extends GetxController { autoScrollTimer?.cancel(); }); mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { - debugPrint(setControllPanel.toString()); if (setControllPanel.value) { isShowControlPanel.value = true; return; @@ -100,16 +110,38 @@ class ReaderController extends GetxController { getContent() async { try { error.value = ''; - watchData.value = null; + // watchData.value = null; watchData.value = await runtime.watch(cuurentPlayUrl) as T; + itemlength[index.value] = (watchData.value as dynamic)?.urls.length; + items[index.value] = (watchData.value as dynamic)?.urls; } catch (e) { error.value = e.toString(); } } - void previousPage() {} + localToGloabalProgress(int localProgress) { + int progress = 0; + for (int i = 0; i < index.value; i++) { + progress += itemlength[i]; + } + progress = localProgress.toInt() + progress; + return progress; + } + + void previousPage(); + + void nextPage(); + void loadNextChapter() {} + void nextChap(); + void prevChap(); - void nextPage() {} + void clearData() { + itemlength.fillRange(0, itemlength.length, 0); + items.fillRange(0, items.length, []); + progress.value = 0; + currentGlobalProgress.value = 0; + currentLocalProgress.value = 0; + } hideControlPanel() { setControllPanel.value = false; diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 0cf3001e..844d4ba3 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -27,6 +27,15 @@ class _ComicReaderContentState extends State { // 按下数量 final List _pointer = []; + // late final List _chapterItems = List.generate( + // _c.playList.length, + // ((val) => Container( + // color: Colors.green, + // width: MediaQuery.of(context).size.width, + // height: MediaQuery.of(context).size.height, + // child: Center(child: Text(val.toString()))))); + // late final List _chapterItems = List.generate( + // _c.playList.length, ((val) => webtoonContent(context, val))); final menuController = fluent.FlyoutController(); final contextAttachKey = GlobalKey(); _buildPlaceholder(BuildContext context) { @@ -43,6 +52,13 @@ class _ComicReaderContentState extends State { ); } + @override + void initState() { + super.initState(); + // ever(_c.index, + // (callback) => _chapterItems[callback] = webtoonContent(context)); + } + Widget _buildDisplay(Widget child) { if (_c.statusBarElement.values.every((element) => element.value == false)) { return child; @@ -67,7 +83,7 @@ class _ComicReaderContentState extends State { if (_c.statusBarElement["reader-settings.page-indicator".i18n]! .value) ...[ Text( - "${_c.currentPage.value + 1}/${_c.watchData.value?.urls.length ?? 0}", + "${_c.currentLocalProgress.value + 1}/${_c.itemlength[_c.index.value]}", style: const TextStyle(color: Colors.white, fontSize: 15), ), const SizedBox(width: 8) @@ -103,6 +119,62 @@ class _ComicReaderContentState extends State { ])); } + Widget webtoonContent(BuildContext context) { + final maxWidth = MediaQuery.of(context).size.width; + final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + return Obx(() => SizedBox( + width: width, + height: height, + child: Listener( + onPointerDown: (event) { + _pointer.add(event.pointer); + if (_pointer.length == 2) { + _c.isZoom.value = true; + } + }, + onPointerUp: (event) { + _pointer.remove(event.pointer); + if (_pointer.length == 1) { + _c.isZoom.value = false; + } + }, + child: InteractiveViewer( + scaleEnabled: _c.isZoom.value, + child: ScrollablePositionedList.builder( + physics: _c.isZoom.value + ? const NeverScrollableScrollPhysics() + : null, + padding: EdgeInsets.symmetric( + horizontal: viewPadding, + ), + initialScrollIndex: _c.currentGlobalProgress.value, + itemScrollController: _c.itemScrollController, + itemPositionsListener: _c.itemPositionsListener, + scrollOffsetController: _c.scrollOffsetController, + scrollOffsetListener: _c.scrollOffsetListener, + itemBuilder: (context, index) { + final img = _c.items.expand((element) => element).toList(); + final url = img[index]; + SizedBox( + width: width, + height: height, + child: const Center( + child: Center( + child: ProgressRing(), + ), + ), + ); + return imageBuilder(url); + }, + itemCount: _c.items.expand((element) => element).length, + ), + ), + ), + )); + } + _buildContent() { late Color backgroundColor; if (Platform.isAndroid) { @@ -142,102 +214,84 @@ class _ComicReaderContentState extends State { } final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; - final images = _c.watchData.value!.urls; final readerType = _c.readType.value; - final cuurentPage = _c.currentPage.value; if (readerType == MangaReadMode.webTonn) { - final width = MediaQuery.of(context).size.width; - final height = MediaQuery.of(context).size.height; return NotificationListener( - onNotification: (scrollEnd) { - final metrics = scrollEnd.metrics; - if (metrics.atEdge) { - bool isTop = metrics.pixels == 0; - if (isTop) { - debugPrint('At the top'); - _c.isScrollEnd.value = false; - } else { - debugPrint('At the bottom'); - _c.isScrollEnd.value = true; - } + child: webtoonContent(context), + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels <= 0; + if (isTop) { + debugPrint('At the top'); + _c.loadPrevChapter(); + } else { + debugPrint('At the bottom'); + _c.loadNextChapter(); } - return true; - }, - child: SingleChildScrollView( - physics: (_c.isScrollEnd.value) - ? null - : const NeverScrollableScrollPhysics(), - child: Column(children: [ - SizedBox( - width: width, - height: height, - child: Listener( - onPointerDown: (event) { - _pointer.add(event.pointer); - if (_pointer.length == 2) { - _c.isZoom.value = true; - } - }, - onPointerUp: (event) { - _pointer.remove(event.pointer); - if (_pointer.length == 1) { - _c.isZoom.value = false; - } - }, - child: InteractiveViewer( - scaleEnabled: _c.isZoom.value, - child: ScrollablePositionedList.builder( - physics: - _c.isZoom.value || _c.isScrollEnd.value - ? const NeverScrollableScrollPhysics() - : null, - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - initialScrollIndex: cuurentPage, - itemScrollController: _c.itemScrollController, - itemPositionsListener: - _c.itemPositionsListener, - scrollOffsetController: - _c.scrollOffsetController, - scrollOffsetListener: _c.scrollOffsetListener, - itemBuilder: (context, index) { - final url = images[index]; - return imageBuilder(url); - }, - itemCount: images.length, - ), - ), - ), - ), - Container( - width: width, - height: height, - color: Colors.green, - ) - ]))); + } + + return true; + }, + ); } //common mode and left to right mode - return ExtendedImageGesturePageView.builder( - itemCount: images.length, - reverse: readerType == MangaReadMode.rightToLeft, - onPageChanged: (index) { - _c.currentPage.value = index; - }, - scrollDirection: Axis.horizontal, - controller: _c.pageController.value, - itemBuilder: (BuildContext context, int index) { - final url = images[index]; - return Container( - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - child: imageBuilder(url), - ); - }, - ); + return Obx(() => NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels <= 0; + if (isTop) { + debugPrint('At the start'); + _c.loadPrevChapter(); + } else { + debugPrint('At the end'); + _c.loadNextChapter(); + } + } + // debugPrint(metrics.pixels.toString()); + return true; + }, + child: ExtendedImageGesturePageView.builder( + itemCount: _c.items.expand((element) => element).length, + reverse: readerType == MangaReadMode.rightToLeft, + onPageChanged: (index) { + _c.currentGlobalProgress.value = index; + }, + scrollDirection: Axis.horizontal, + controller: _c.pageController.value, + itemBuilder: (BuildContext context, int index) { + // final urls = _c.items[_c.index.value]; + // final url = images[index]; + // if (index == 0) { + // return Container( + // padding: EdgeInsets.symmetric( + // horizontal: viewPadding, + // ), + // color: Colors.red, + // ); + // } + // if (index == _c.itemlength[_c.index.value] - 1) { + // return Container( + // padding: EdgeInsets.symmetric( + // horizontal: viewPadding, + // ), + // color: Colors.red, + // ); + // } + final img = + _c.items.expand((element) => element).toList(); + final url = img[index]; + return Container( + padding: EdgeInsets.symmetric( + horizontal: viewPadding, + ), + child: imageBuilder(url), + ); + }, + ))); }); }), ), @@ -305,6 +359,11 @@ class _ComicReaderContentState extends State { fit: BoxFit.fitWidth, placeholder: _buildPlaceholder(context), headers: _c.watchData.value?.headers, + initGestureConfigHandler: (state) { + return GestureConfig( + inPageView: true, + ); + }, )); } diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index 5911c85c..f8061b59 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -181,7 +181,7 @@ class _ComicReaderSettingsState extends State { MiruStorage.setSetting( SettingKey.prevPageHitBox, val); }), - Obx(() => SettingsSwitchTile( + SettingsSwitchTile( icon: const Icon(Icons.coffee), title: "reader-settings.enable-wakelock".i18n, buildValue: () => _c.enableWakeLock.value, @@ -190,7 +190,7 @@ class _ComicReaderSettingsState extends State { _c.enableWakeLock.value = val; MiruStorage.setSetting( SettingKey.enableWakelock, val); - })), + }), ], )), ), diff --git a/lib/views/widgets/cache_network_image.dart b/lib/views/widgets/cache_network_image.dart index 8b5d413d..8e685b79 100644 --- a/lib/views/widgets/cache_network_image.dart +++ b/lib/views/widgets/cache_network_image.dart @@ -24,6 +24,7 @@ class CacheNetWorkImagePic extends StatelessWidget { this.placeholder, this.canFullScreen = false, this.mode = ExtendedImageMode.none, + this.initGestureConfigHandler, }); final String url; final BoxFit fit; @@ -34,7 +35,7 @@ class CacheNetWorkImagePic extends StatelessWidget { final bool canFullScreen; final Widget? placeholder; final ExtendedImageMode mode; - + final InitGestureConfigHandler? initGestureConfigHandler; _errorBuild() { if (fallback != null) { return fallback!; @@ -52,6 +53,7 @@ class CacheNetWorkImagePic extends StatelessWidget { height: height, cache: true, mode: mode, + initGestureConfigHandler: initGestureConfigHandler, loadStateChanged: (state) { switch (state.extendedImageLoadState) { case LoadState.loading: diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index c556dc59..b231839f 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -35,12 +35,12 @@ class _ControlPanelFooterState ? _c.watchData.value?.content.length ?? 0 : _c.watchData.value?.urls.length ?? 0; }); - ever(_c.progress, (callback) { - progressObs.value = callback; - }); - ever(_c.isShowControlPanel, (callback) { - // debugPrint("scrolled ${c.progress.value}"); + ever(_c.index, (callback) { progressObs.value = _c.progress.value; + totalObs.value = _c.itemlength[_c.index.value]; + }); + ever(_c.currentLocalProgress, (callback) { + progressObs.value = callback; }); } @@ -48,7 +48,7 @@ class _ControlPanelFooterState final _desktopIntervalFlyoutController = fluent.FlyoutController(); Widget _buildAndroid(BuildContext context) { final double width = MediaQuery.of(context).size.width; - + // return Container(); return Align( alignment: const Alignment(0, 1), child: TweenAnimationBuilder( @@ -70,7 +70,7 @@ class _ControlPanelFooterState ), child: IconButton( onPressed: () { - _c.index.value--; + _c.prevChap(); }, icon: const Icon(Icons.skip_previous_rounded))), @@ -85,21 +85,24 @@ class _ControlPanelFooterState if (totalObs.value != 0 || !_c.isShowControlPanel.value) { return Slider( - label: (progressObs.value + 1).toString(), - max: (totalObs.value - 1) < 0 + label: (_c.currentLocalProgress.value + 1) + .toString(), + max: _c.itemlength[_c.index.value] < 1 ? 1 - : (totalObs.value - 1).toDouble(), + : (_c.itemlength[_c.index.value] - 1) + .toDouble(), min: 0, divisions: (totalObs.value - 1) < 0 ? 1 : totalObs.value - 1, - value: progressObs.value.toDouble(), - onChanged: _c.isShowControlPanel.value - ? (val) { - _c.updateSlider.value = true; - _c.progress.value = val.toInt(); - } - : null, + value: _c.currentLocalProgress.value + .toDouble(), + onChanged: (val) { + _c.updateSlider.value = true; + _c.progress.value = + _c.localToGloabalProgress( + val.toInt()); + }, ); } return const Slider( @@ -116,7 +119,7 @@ class _ControlPanelFooterState ), child: IconButton( onPressed: () { - _c.index.value++; + _c.nextChap(); }, icon: const Icon(Icons.skip_next_rounded))) ])))), diff --git a/lib/views/widgets/watch/desktop_command_bar.dart b/lib/views/widgets/watch/desktop_command_bar.dart index 18bc79db..e28c9e36 100644 --- a/lib/views/widgets/watch/desktop_command_bar.dart +++ b/lib/views/widgets/watch/desktop_command_bar.dart @@ -1,18 +1,18 @@ -import 'package:flutter/material.dart'; +// import 'package:flutter/material.dart'; -class DesktopCommandBar extends StatelessWidget { - const DesktopCommandBar( - {super.key, required this.text, this.icon, required this.onPressed}); - final Widget text; - final Widget? icon; - final VoidCallback onPressed; +// class DesktopCommandBar extends StatelessWidget { +// const DesktopCommandBar( +// {super.key, required this.text, this.icon, required this.onPressed}); +// final Widget text; +// final Widget? icon; +// final VoidCallback onPressed; - @override - Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(4), - child: Row( - children: [], - )); - } -} +// @override +// Widget build(BuildContext context) { +// return Padding( +// padding: const EdgeInsets.all(4), +// child: Row( +// children: [], +// )); +// } +// } From c2740104915187e3c3a10e287b95b627c5facbdb Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Thu, 8 Feb 2024 23:32:04 +0800 Subject: [PATCH 12/24] add infinite scroll for novel --- android/app/src/main/AndroidManifest.xml | 6 +- assets/i18n/en.json | 3 +- lib/controllers/watch/comic_controller.dart | 66 +-- lib/controllers/watch/novel_controller.dart | 91 +++- lib/controllers/watch/reader_controller.dart | 85 +++- .../reader/novel/novel_reader_content.dart | 396 ++++++++++-------- .../widgets/watch/control_panel_footer.dart | 5 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 8 + pubspec.yaml | 2 +- .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 12 files changed, 403 insertions(+), 265 deletions(-) diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 73bc7f29..8897b00b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -33,5 +33,9 @@ - + + + + + \ No newline at end of file diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 176a71c4..b5d053d6 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -205,7 +205,8 @@ "battery-icon": "Battery Icon" }, "novel-settings": { - "font-size": "Font size" + "font-size": "Font size", + "line":"Lines" }, "bugreport": { "auto-remove-subtitle": "delete in ~ days", diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index ff37e516..c033229d 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -11,9 +11,7 @@ import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; import 'package:miru_app/utils/miru_storage.dart'; import 'dart:async'; -import 'package:battery_plus/battery_plus.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; -import 'package:miru_app/utils/i18n.dart'; import 'package:window_manager/window_manager.dart'; class ComicController extends ReaderController { @@ -39,31 +37,12 @@ class ComicController extends ReaderController { // 当前页码 final pageController = ExtendedPageController().obs; final itemPositionsListener = ItemPositionsListener.create(); - final alignMode = Alignment.bottomLeft.obs; // 是否已经恢复上次阅读 final isRecover = false.obs; - final batteryLevel = 100.obs; final readType = MangaReadMode.standard.obs; final globalScrollController = ScrollController(); - Timer? _barreryTimer; final currentOffset = 0.0.obs; - final statusBarElement = { - 'reader-settings.battery'.i18n: true.obs, - 'reader-settings.time'.i18n: true.obs, - 'reader-settings.page-indicator'.i18n: true.obs, - 'reader-settings.battery-icon'.i18n: true.obs, - }; final isZoom = false.obs; - final currentTime = "".obs; - Future _statusBar() async { - final battery = Battery(); - batteryLevel.value = await battery.batteryLevel; - final datenow = DateTime.now(); - final hour = datenow.hour < 10 ? "0${datenow.hour}" : datenow.hour; - final minute = datenow.minute < 10 ? "0${datenow.minute}" : datenow.minute; - currentTime.value = "$hour:$minute"; - } - final isScrollEnd = false.obs; @override @@ -80,9 +59,7 @@ class ComicController extends ReaderController { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); enableWakeLock.value = MiruStorage.getSetting(SettingKey.enableWakelock); WakelockPlus.toggle(enable: enableWakeLock.value); - await _statusBar(); - _barreryTimer = - Timer.periodic(const Duration(seconds: 10), (timer) => _statusBar()); + itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; @@ -104,8 +81,6 @@ class ComicController extends ReaderController { callback, ); }); - // 如果切换章节,重置当前页码 - // ever(super.index, (callback) => currentPage.value = 0); //control footer 的 slider 改變時,更新頁碼 ever(progress, (callback) { // 防止逆向回饋 @@ -136,7 +111,7 @@ class ComicController extends ReaderController { if (isRecover.value || callback == null) { return; } - getTartgetContent(playIndex); + loadTargetContent(playIndex); isRecover.value = true; // 获取上次阅读的页码 final history = await DatabaseService.getHistoryByPackageAndUrl( @@ -157,20 +132,17 @@ class ComicController extends ReaderController { super.onInit(); } - getTartgetContent(int targetIndex) async { + @override + Future loadTargetContent(int targetIndex) async { try { if (targetIndex < 0 || targetIndex == itemlength.length) { return; } final dynamic updatedData = await runtime.watch(playList[targetIndex].url); - // if (targetIndex < index.value && items[targetIndex].isEmpty) { - // _jumpPage(itemlength[targetIndex] + currentGlobalProgress.value); - // } items[targetIndex] = updatedData.urls as List; itemlength[targetIndex] = updatedData.urls.length; isScrollEnd.value = false; - // index.value = targetIndex; } catch (e) { error.value = e.toString(); } @@ -277,12 +249,13 @@ class ComicController extends ReaderController { @override Future loadNextChapter() async { - await getTartgetContent(index.value + 1); + await loadTargetContent(index.value + 1); return; } + @override Future loadPrevChapter() async { - await getTartgetContent(index.value - 1); + await loadTargetContent(index.value - 1); if (itemScrollController.isAttached) { itemScrollController.scrollTo( index: itemlength[index.value - 1], @@ -315,7 +288,7 @@ class ComicController extends ReaderController { mediaId: anilistID, ); } - _barreryTimer!.cancel(); + mouseTimer?.cancel(); WakelockPlus.disable(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); @@ -326,18 +299,15 @@ class ComicController extends ReaderController { } @override - void nextChap() { - watchData.value = null; - clearData(); - index.value++; - getContent(); - } - - @override - void prevChap() { - watchData.value = null; - clearData(); - index.value--; - getContent(); + Future getContent() async { + try { + error.value = ''; + watchData.value = + await runtime.watch(cuurentPlayUrl) as ExtensionMangaWatch; + itemlength[index.value] = (watchData.value as dynamic)?.urls.length; + items[index.value] = (watchData.value as dynamic)?.urls; + } catch (e) { + error.value = e.toString(); + } } } diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index 43c13cc5..d88038aa 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -6,6 +6,8 @@ import 'package:miru_app/utils/miru_storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:flutter/services.dart'; +import 'package:flutter_tts/flutter_tts.dart'; +import 'package:flutter/material.dart'; class NovelController extends ReaderController { NovelController({ @@ -23,23 +25,28 @@ class NovelController extends ReaderController { final fontSize = (18.0).obs; final itemPositionsListener = ItemPositionsListener.create(); final isRecover = false.obs; - final positions = 0.obs; - + late final FlutterTts flutterTts; + final RxBool enableSelectText = false.obs; @override - void onInit() { + void onInit() async { super.onInit(); + getContent(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); + flutterTts = FlutterTts(); fontSize.value = MiruStorage.getSetting(SettingKey.novelFontSize); WakelockPlus.toggle( enable: MiruStorage.getSetting(SettingKey.enableWakelock)); + List languages = await flutterTts.getLanguages; + debugPrint(languages.toString()); itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; } final pos = itemPositionsListener.itemPositions.value.first; - positions.value = pos.index; + currentGlobalProgress.value = pos.index; }); scrollOffsetListener.changes.listen((event) { + enableSelectText.value = false; hideControlPanel(); }); ever( @@ -48,7 +55,6 @@ class NovelController extends ReaderController { ); // 切换章节时重置页码 - ever(index, (callback) => positions.value = 0); ever(super.watchData, (callback) async { if (isRecover.value || callback == null) { return; @@ -66,38 +72,55 @@ class NovelController extends ReaderController { history.episodeId != index.value) { return; } - positions.value = int.parse(history.progress); - _jumpLine(positions.value); + currentGlobalProgress.value = int.parse(history.progress); + _jumpLine(currentGlobalProgress.value); }); ever(progress, (callback) { // 防止逆向回饋 if (!updateSlider.value) { return; } - positions.value = callback; + currentGlobalProgress.value = callback; _jumpLine(callback); }); - ever(positions, (callback) { - progress.value = callback; + ever(currentGlobalProgress, (callback) { + if (updateSlider.value) { + progress.value = callback; + } updateSlider.value = false; + int fullIndex = 0; + // debugPrint(currentLocalProgress.value.toString()); + for (int i = 0; i < itemlength.length; i++) { + fullIndex += itemlength[i]; + if (fullIndex > callback) { + index.value = i; + super.index.value = i; + currentLocalProgress.value = callback - (fullIndex - itemlength[i]); + break; + } + } }); ever(super.watchData, (callback) async { if (isRecover.value || callback == null) { return; } + loadTargetContent(playIndex); isRecover.value = true; // 获取上次阅读的页码 final history = await DatabaseService.getHistoryByPackageAndUrl( super.runtime.extension.package, super.detailUrl, ); + if (history == null || history.progress.isEmpty || episodeGroupId != history.episodeGroupId || history.episodeId != index.value) { return; } - positions.value = int.parse(history.progress); + currentGlobalProgress.value = int.parse(history.progress); + _jumpLine(currentGlobalProgress.value); + // jumpScroller(index.value); }); } @@ -113,7 +136,7 @@ class NovelController extends ReaderController { if (super.watchData.value != null) { final totalProgress = watchData.value!.content.length.toString(); super.addHistory( - positions.value.toString(), + currentGlobalProgress.value.toString(), totalProgress, ); } @@ -123,11 +146,47 @@ class NovelController extends ReaderController { } @override - void nextPage() {} + Future loadTargetContent(int targetIndex) async { + try { + if (targetIndex < 0 || targetIndex == itemlength.length) { + return; + } + final dynamic updatedData = + await runtime.watch(playList[targetIndex].url); + items[targetIndex] = updatedData.content as List; + itemlength[targetIndex] = updatedData.content.length; + } catch (e) { + error.value = e.toString(); + } + } + @override - void previousPage() {} + Future loadNextChapter() async { + await loadTargetContent(index.value + 1); + return; + } + @override - void nextChap() {} + Future loadPrevChapter() async { + await loadTargetContent(index.value - 1); + if (itemScrollController.isAttached) { + itemScrollController.scrollTo( + index: itemlength[index.value - 1], + duration: const Duration(milliseconds: 10)); + return; + } + } + @override - void prevChap() {} + Future getContent() async { + try { + error.value = ''; + watchData.value = + await runtime.watch(cuurentPlayUrl) as ExtensionFikushonWatch; + itemlength[index.value] = (watchData.value as dynamic)?.content.length; + items[index.value] = (watchData.value as dynamic)?.content; + } catch (e) { + error.value = e.toString(); + } + } } diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index b9b1e9cf..0cec1776 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -7,6 +7,8 @@ import 'package:miru_app/data/services/extension_service.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/utils/miru_storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:miru_app/utils/i18n.dart'; +import 'package:battery_plus/battery_plus.dart'; abstract class ReaderController extends GetxController { final String title; @@ -44,6 +46,7 @@ abstract class ReaderController extends GetxController { final updateSlider = true.obs; final isInfinityScrollMode = false.obs; final isLoading = false.obs; + Timer? _barreryTimer; //點擊區域是否反轉 final RxBool tapRegionIsReversed = false.obs; final dynamic _nextPageHitBox = @@ -70,13 +73,35 @@ abstract class ReaderController extends GetxController { late final List itemlength = List.filled(playList.length, 0); final currentGlobalProgress = 0.obs; final currentLocalProgress = 0.obs; + final statusBarElement = { + 'reader-settings.battery'.i18n: true.obs, + 'reader-settings.time'.i18n: true.obs, + 'reader-settings.page-indicator'.i18n: true.obs, + 'reader-settings.battery-icon'.i18n: true.obs, + }; + final batteryLevel = 100.obs; + final currentTime = ''.obs; + Future _statusBar() async { + final battery = Battery(); + batteryLevel.value = await battery.batteryLevel; + final datenow = DateTime.now(); + final hour = datenow.hour < 10 ? "0${datenow.hour}" : datenow.hour; + final minute = datenow.minute < 10 ? "0${datenow.minute}" : datenow.minute; + currentTime.value = "$hour:$minute"; + } + + final alignMode = Alignment.bottomLeft.obs; + @override - void onInit() { + void onInit() async { // getContent(); autoScrollInterval.value = _autoScrollInterval; autoScrollOffset.value = _autoScrollOffset; nextPageHitBox.value = _nextPageHitBox; prevPageHitBox.value = _prevPageHitBox; + await _statusBar(); + _barreryTimer = + Timer.periodic(const Duration(seconds: 10), (timer) => _statusBar()); // ever(index, (callback) { // getContent(); // }); @@ -107,19 +132,7 @@ abstract class ReaderController extends GetxController { super.onInit(); } - getContent() async { - try { - error.value = ''; - // watchData.value = null; - watchData.value = await runtime.watch(cuurentPlayUrl) as T; - itemlength[index.value] = (watchData.value as dynamic)?.urls.length; - items[index.value] = (watchData.value as dynamic)?.urls; - } catch (e) { - error.value = e.toString(); - } - } - - localToGloabalProgress(int localProgress) { + int localToGloabalProgress(int localProgress) { int progress = 0; for (int i = 0; i < index.value; i++) { progress += itemlength[i]; @@ -128,16 +141,42 @@ abstract class ReaderController extends GetxController { return progress; } - void previousPage(); + int globalToLocalProgress(int globalProgress) { + int progress = globalProgress; + for (int i = 0; i < index.value; i++) { + if (globalProgress < itemlength[i]) { + break; + } + globalProgress -= itemlength[i]; + } + debugPrint(progress.toString()); + return progress; + } + + void previousPage() {} + void nextPage() {} + void loadNextChapter(); + void loadPrevChapter(); - void nextPage(); - void loadNextChapter() {} - void nextChap(); - void prevChap(); + void nextChap() { + clearData(); + index.value++; + getContent(); + } + + void prevChap() { + clearData(); + index.value--; + getContent(); + } + + Future getContent(); + Future loadTargetContent(int targetIndex); void clearData() { itemlength.fillRange(0, itemlength.length, 0); items.fillRange(0, items.length, []); + watchData.value = null; progress.value = 0; currentGlobalProgress.value = 0; currentLocalProgress.value = 0; @@ -163,4 +202,12 @@ abstract class ReaderController extends GetxController { ); await Get.find().onRefresh(); } + + @override + void onClose() { + _barreryTimer?.cancel(); + autoScrollTimer?.cancel(); + mouseTimer?.cancel(); + super.onClose(); + } } diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index 7ebfe29e..48cebea9 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -7,6 +7,7 @@ import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/progress.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:based_battery_indicator/based_battery_indicator.dart'; class NovelReaderContent extends StatefulWidget { const NovelReaderContent(this.tag, {super.key}); @@ -18,204 +19,249 @@ class NovelReaderContent extends StatefulWidget { class _NovelReaderContentState extends State { late final _c = Get.find(tag: widget.tag); + Widget _buildDisplay(Widget child) { + if (_c.statusBarElement.values.every((element) => element.value == false)) { + return child; + } + return Stack( + children: [ + child, + Obx(() => Align( + alignment: _c.alignMode.value, + child: Container( + color: Colors.black.withAlpha(200), + padding: const EdgeInsets.fromLTRB(20, 2, 12, 2), + child: _indicatorBuilder(), + ), + )), + ], + ); + } - _buildContent() { - return GestureDetector( - onTapDown: (detail) { - _c.setControllPanel.value = !_c.setControllPanel.value; - }, - child: LayoutBuilder( - builder: (context, constraints) => Obx( - () { - // // 宽度 大于 800 就是整体宽度的一半 - final maxWidth = constraints.maxWidth; - // final width = maxWidth > 800 ? maxWidth / 2 : maxWidth; - // final height = constraints.maxHeight; - if (_c.error.value.isNotEmpty) { - return SizedBox( - width: double.infinity, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(_c.error.value), - const SizedBox(height: 20), - PlatformButton( - child: Text('common.retry'.i18n), - onPressed: () { - _c.getContent(); - }, - ) - ], - ), - ); - } + Widget _buildContent() { + return Stack(children: [ + GestureDetector( + onTapDown: (detail) { + _c.setControllPanel.value = !_c.setControllPanel.value; + }, + child: LayoutBuilder( + builder: (context, constraints) => Obx( + () { + // // 宽度 大于 800 就是整体宽度的一半 + final maxWidth = constraints.maxWidth; + // final width = maxWidth > 800 ? maxWidth / 2 : maxWidth; + // final height = constraints.maxHeight; + if (_c.error.value.isNotEmpty) { + return SizedBox( + width: double.infinity, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text(_c.error.value), + const SizedBox(height: 20), + PlatformButton( + child: Text('common.retry'.i18n), + onPressed: () { + _c.getContent(); + }, + ) + ], + ), + ); + } - if (_c.watchData.value == null) { - return const Center(child: ProgressRing()); - } + if (_c.watchData.value == null) { + return const Center(child: ProgressRing()); + } - final watchData = _c.watchData.value!; + final watchData = _c.watchData.value!; - final listviewPadding = - maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; + final listviewPadding = + maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; - final fontSize = _c.fontSize.value; + final fontSize = _c.fontSize.value; - return Center( - child: ScrollablePositionedList.builder( - itemPositionsListener: _c.itemPositionsListener, - initialScrollIndex: _c.positions.value, - itemScrollController: _c.itemScrollController, - scrollOffsetController: _c.scrollOffsetController, - padding: EdgeInsets.symmetric( - horizontal: listviewPadding, - vertical: 16, - ), - itemBuilder: (context, index) { - if (index == 0) { - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text( - _c.title + _c.playList[_c.playIndex].name, - style: const TextStyle(fontSize: 26), + return Center( + child: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels <= 0; + if (isTop) { + debugPrint('At the top'); + _c.loadPrevChapter(); + } else { + debugPrint('At the bottom'); + _c.loadNextChapter(); + } + } + + return true; + }, + child: ScrollablePositionedList.builder( + itemPositionsListener: _c.itemPositionsListener, + initialScrollIndex: _c.currentGlobalProgress.value, + itemScrollController: _c.itemScrollController, + scrollOffsetController: _c.scrollOffsetController, + padding: EdgeInsets.symmetric( + horizontal: listviewPadding, + vertical: 16, ), - ); - } - if (index == 1) { - return (watchData.subtitle != null) - ? Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text( - watchData.subtitle!, - style: const TextStyle(fontSize: 20), + itemBuilder: (context, index) { + if (_c.globalToLocalProgress(index) == 0) { + return Column(children: [ + const SizedBox( + height: 20, ), - ) - : const SizedBox(); - } - return Padding( - padding: const EdgeInsets.only(bottom: 20), - child: SelectableText.rich( - TextSpan( - children: [ - const WidgetSpan(child: SizedBox(width: 40.0)), - TextSpan( - text: watchData.content[index - 2], - style: TextStyle( - fontSize: fontSize, - fontWeight: FontWeight.w400, - height: 2, - textBaseline: TextBaseline.ideographic, - fontFamily: 'Microsoft Yahei', + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text( + _c.title + _c.playList[_c.index.value].name, + style: const TextStyle(fontSize: 26), + ), ), - ), - ], - ), - ), - ); - }, - itemCount: watchData.content.length + 2, - ), - ); - - // const TextStyle textStyle = TextStyle( - // fontSize: 18, - // fontWeight: FontWeight.w400, - // height: 2, - // textBaseline: TextBaseline.ideographic, - // ); - - // // 获取每句子的高 - // final List heightList = []; - // for (final String sentence in content) { - // final TextPainter painter = TextPainter( - // text: TextSpan( - // text: sentence, - // style: textStyle, - // ), - // textDirection: TextDirection.ltr, - // )..layout(maxWidth: width - 140); - // heightList.add(painter.height); - // } - - // // 通过高度判断每页能放多少句子 - // final List pageSentenceCount = []; - // double pageHeight = 0; - // int sentenceCount = 0; - // for (final double textHeight in heightList) { - // pageHeight += textHeight; - // sentenceCount++; - // if (pageHeight > height) { - // pageSentenceCount.add(sentenceCount); - // pageHeight = 0; - // sentenceCount = 0; - // } - // } - - // final List pageViewList = []; + if (watchData.subtitle != null) ...[ + Padding( + padding: const EdgeInsets.only(bottom: 20), + child: Text( + watchData.subtitle!, + style: const TextStyle(fontSize: 20), + ), + ) + ], + _textContent(index, fontSize) + ]); + } + return _textContent(index, fontSize); + }, + itemCount: _c.items.expand((element) => element).length, + )), + ); + }, + ), + )) + ]); + } - // int pageStartIndex = 0; - // for (final int sentenceCount in pageSentenceCount) { - // final List pageContent = content.sublist( - // pageStartIndex, - // pageStartIndex + sentenceCount, - // ); - // pageStartIndex += sentenceCount; - // pageViewList.add( - // ListView.builder( - // shrinkWrap: true, - // physics: const NeverScrollableScrollPhysics(), - // itemBuilder: (context, index) { - // return Text( - // pageContent[index], - // style: textStyle, - // ); - // }, - // itemCount: pageContent.length, - // ), - // ); - // } + Widget _indicatorBuilder() { + return Obx(() => Row(mainAxisSize: MainAxisSize.min, children: [ + if (_c.statusBarElement["reader-settings.page-indicator".i18n]! + .value) ...[ + Text( + "${_c.currentLocalProgress.value + 1} ${"novel-settings.line".i18n}", + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + const SizedBox(width: 8) + ], + if (_c.statusBarElement["reader-settings.battery-icon".i18n]! + .value) ...[ + BasedBatteryIndicator( + status: BasedBatteryStatus( + value: _c.batteryLevel.value, + type: BasedBatteryStatusType.normal, + ), + trackHeight: 10.0, + trackAspectRatio: 2.0, + curve: Curves.ease, + duration: const Duration(seconds: 10), + ), + const SizedBox(width: 8) + ], + if (_c.statusBarElement["reader-settings.battery".i18n]!.value) ...[ + Text( + "${_c.batteryLevel.value}%", + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + const SizedBox(width: 8) + ], + if (_c.statusBarElement["reader-settings.time".i18n]!.value) ...[ + Text( + _c.currentTime.value, + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + const SizedBox(width: 8) + ], + ])); + } - // return PageView( - // children: [ - // // 如果大于 800 就是整体宽度的一半 - // for (var i = 0; - // i < pageViewList.length; - // maxWidth > 800 ? i += 2 : i++) - // Row( - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // Expanded( - // child: Container( - // child: pageViewList[i], - // ), - // ), - // if (maxWidth > 800) - // i + 1 < pageViewList.length - // ? Expanded( - // child: Container( - // child: pageViewList[i + 1], - // ), - // ) - // : const Expanded(child: SizedBox()), - // ], - // ) - // ], - // ); - }, + Widget _textContent(int index, double fontSize) { + final content = _c.items.expand((element) => element).toList(); + return Padding( + padding: const EdgeInsets.only(bottom: 20), + child: SelectableText.rich( + onTap: () { + _c.setControllPanel.value = !_c.setControllPanel.value; + }, + TextSpan( + children: [ + const WidgetSpan(child: SizedBox(width: 40.0)), + TextSpan( + text: content[index], + style: TextStyle( + fontSize: fontSize, + fontWeight: FontWeight.w400, + height: 2, + textBaseline: TextBaseline.ideographic, + fontFamily: 'Microsoft Yahei', + ), + ), + ], ), )); + // Obx(() => Column( + // children: [ + // _c.enableSelectText.value + // ? SelectableText.rich( + // onTap: () { + // _c.setControllPanel.value = + // !_c.setControllPanel.value; + // }, + // TextSpan( + // children: [ + // const WidgetSpan(child: SizedBox(width: 40.0)), + // TextSpan( + // text: content[index], + // style: TextStyle( + // fontSize: fontSize, + // fontWeight: FontWeight.w400, + // height: 2, + // textBaseline: TextBaseline.ideographic, + // fontFamily: 'Microsoft Yahei', + // ), + // ), + // ], + // ), + // ) + // : GestureDetector( + // onTap: () { + // _c.setControllPanel.value = + // !_c.setControllPanel.value; + // _c.enableSelectText.value = true; + // }, + // child: Text( + // content[index], + // style: TextStyle( + // fontSize: fontSize, + // fontWeight: FontWeight.w400, + // height: 2, + // textBaseline: TextBaseline.ideographic, + // fontFamily: 'Microsoft Yahei', + // ), + // )) + // ], + // ))); } Widget _buildAndroid(BuildContext context) { return Scaffold( - body: SafeArea(child: _buildContent()), + body: SafeArea(child: _buildDisplay(_buildContent())), ); } Widget _buildDesktop(BuildContext context) { return Container( color: fluent.FluentTheme.of(context).micaBackgroundColor, - child: _buildContent(), + child: _buildDisplay(_buildContent()), ); } diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index b231839f..f0bbfb45 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -31,9 +31,7 @@ class _ControlPanelFooterState super.initState(); ever(_c.watchData, (callback) { progressObs.value = 0; - totalObs.value = (T == NovelController) - ? _c.watchData.value?.content.length ?? 0 - : _c.watchData.value?.urls.length ?? 0; + totalObs.value = _c.itemlength[_c.index.value]; }); ever(_c.index, (callback) { progressObs.value = _c.progress.value; @@ -135,7 +133,6 @@ class _ControlPanelFooterState } Widget _buildDesktop(BuildContext context) { - final double width = MediaQuery.of(context).size.width; final double height = MediaQuery.of(context).size.height; return Align( alignment: const Alignment(0, 1), diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 5d9d8f79..0093404b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,6 +10,7 @@ import desktop_multi_window import device_info_plus import flutter_inappwebview_macos import flutter_js +import flutter_tts import isar_flutter_libs import media_kit_libs_macos_video import media_kit_video @@ -28,6 +29,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterJsPlugin.register(with: registry.registrar(forPlugin: "FlutterJsPlugin")) + FlutterTtsPlugin.register(with: registry.registrar(forPlugin: "FlutterTtsPlugin")) IsarFlutterLibsPlugin.register(with: registry.registrar(forPlugin: "IsarFlutterLibsPlugin")) MediaKitLibsMacosVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitLibsMacosVideoPlugin")) MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) diff --git a/pubspec.lock b/pubspec.lock index cfe0aa71..a56270c7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -552,6 +552,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_tts: + dependency: "direct main" + description: + name: flutter_tts + sha256: cbb3fd43b946e62398560235469e6113e4fe26c40eab1b7cb5e7c417503fb3a8 + url: "https://pub.dev" + source: hosted + version: "3.8.5" flutter_web_plugins: dependency: transitive description: flutter diff --git a/pubspec.yaml b/pubspec.yaml index 7e3418b4..ba7ec4f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -62,7 +62,7 @@ dependencies: wakelock_plus: ^1.1.4 battery_plus: ^5.0.2 based_battery_indicator: ^1.0.3 - + flutter_tts: ^3.8.5 dev_dependencies: flutter_test: sdk: flutter diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index c9e9bd24..761360b5 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -26,6 +27,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("DesktopMultiWindowPlugin")); FlutterJsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterJsPlugin")); + FlutterTtsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterTtsPlugin")); FlutterWindowsWebviewPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterWindowsWebviewPluginCApi")); IsarFlutterLibsPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index f3002f91..7d21ff90 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST battery_plus desktop_multi_window flutter_js + flutter_tts flutter_windows_webview isar_flutter_libs media_kit_libs_windows_video From 6575ec030d6a6e4fa2fe4f259504d611765b75d7 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sat, 10 Feb 2024 01:52:31 +0800 Subject: [PATCH 13/24] initial tts support for android --- assets/i18n/en.json | 15 +- lib/controllers/watch/comic_controller.dart | 22 +- lib/controllers/watch/novel_controller.dart | 62 +++- lib/controllers/watch/reader_controller.dart | 40 +-- lib/utils/color.dart | 13 + lib/utils/miru_storage.dart | 9 + .../reader/comic/comic_reader_content.dart | 2 +- .../reader/novel/novel_reader_content.dart | 18 +- .../reader/novel/novel_reader_settings.dart | 271 ++++++++++++++++-- .../widgets/watch/control_panel_header.dart | 7 + lib/views/widgets/watch/reader_view.dart | 8 +- 11 files changed, 383 insertions(+), 84 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index b5d053d6..b405dfe6 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -45,7 +45,9 @@ "login": "Login", "no-data": "No data", "clear": "Clear", - "export": "Export" + "export": "Export", + "tts":"Text to speech", + "common": "Common" }, "home": { "continue-watching": "Continue", @@ -202,11 +204,18 @@ "battery": "Battery", "time": "Time", "page-indicator": "Page Indicator", - "battery-icon": "Battery Icon" + "battery-icon": "Battery Icon", + "indicator-alignment": "Indicator Alignment" }, "novel-settings": { "font-size": "Font size", - "line":"Lines" + "line":"Lines", + "enable-tts": "Enable TTS", + "tts-rate": "TTS Rate", + "tts-lang": "TTS Language", + "tts-volume": "TTS Volume", + "tts-pitch": "TTS Pitch", + "text-color": "Text Color" }, "bugreport": { "auto-remove-subtitle": "delete in ~ days", diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index c033229d..65514d88 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -70,9 +70,9 @@ class ComicController extends ReaderController { scrollOffsetListener.changes.listen((event) { hideControlPanel(); }); - ever(height, (callback) { - super.height.value = callback; - }); + // ever(height, (callback) { + // super.height.value = callback; + // }); ever(readType, (callback) { _jumpPage(currentGlobalProgress.value); // 保存设置 @@ -81,6 +81,22 @@ class ComicController extends ReaderController { callback, ); }); + ever(enableAutoScroll, (callback) { + if (callback) { + autoScrollTimer = Timer.periodic( + Duration(milliseconds: autoScrollInterval.value), (timer) { + if (isScrolled.value) { + scrollOffsetController.animateScroll( + duration: const Duration(milliseconds: 100), + curve: Curves.ease, + offset: autoScrollOffset.value, + ); + } + }); + return; + } + autoScrollTimer?.cancel(); + }); //control footer 的 slider 改變時,更新頁碼 ever(progress, (callback) { // 防止逆向回饋 diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index d88038aa..f1e6ee8d 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:get/get.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; @@ -23,21 +25,43 @@ class NovelController extends ReaderController { // 字体大小 final fontSize = (18.0).obs; + final ttsRate = 0.3.obs; + final ttsVolume = 0.3.obs; + final ttsPitch = 0.3.obs; final itemPositionsListener = ItemPositionsListener.create(); final isRecover = false.obs; late final FlutterTts flutterTts; final RxBool enableSelectText = false.obs; + final RxList ttsLang = [].obs; + final RxString ttsLangValue = ''.obs; + final playBackIsComplete = false; + late final RxList subtitles = + List.generate(playList.length, (index) => "").obs; + final Rx textColor = Colors.white.obs; + initTts() { + ttsVolume.value = MiruStorage.getSetting(SettingKey.ttsVolume); + ttsRate.value = MiruStorage.getSetting(SettingKey.ttsRate); + ttsPitch.value = MiruStorage.getSetting(SettingKey.ttsPitch); + flutterTts = FlutterTts(); + flutterTts.awaitSpeakCompletion(true); + flutterTts.setCompletionHandler(() { + debugPrint("completed"); + }); + } + @override void onInit() async { super.onInit(); getContent(); + initTts(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); - flutterTts = FlutterTts(); fontSize.value = MiruStorage.getSetting(SettingKey.novelFontSize); + // textColor.value = MiruStorage.getSetting(SettingKey.textColor); WakelockPlus.toggle( enable: MiruStorage.getSetting(SettingKey.enableWakelock)); - List languages = await flutterTts.getLanguages; - debugPrint(languages.toString()); + ttsLangValue.value = MiruStorage.getSetting(SettingKey.ttsLanguage); + ttsLang.value = await flutterTts.getLanguages; + debugPrint(ttsLang.toString()); itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; @@ -83,6 +107,26 @@ class NovelController extends ReaderController { currentGlobalProgress.value = callback; _jumpLine(callback); }); + // tts 播放 + ever(enableAutoScroll, (callback) async { + await flutterTts.setLanguage(ttsLangValue.value); + await flutterTts.setSpeechRate(ttsRate.value); + await flutterTts.setVolume(ttsVolume.value); + await flutterTts.setPitch(ttsPitch.value); + for (int i = currentLocalProgress.value; + i < itemlength[index.value]; + i++) { + if (!enableAutoScroll.value) { + await flutterTts.stop(); + break; + } + final readingProgress = items[index.value][i]; + debugPrint("current reading: $readingProgress , progress: $i"); + animeScrollTo(localToGloabalProgress(i) - 1); + await flutterTts.speak(items[index.value][i]); + } + enableAutoScroll.value = false; + }); ever(currentGlobalProgress, (callback) { if (updateSlider.value) { progress.value = callback; @@ -131,6 +175,13 @@ class NovelController extends ReaderController { itemScrollController.jumpTo(index: index); } + animeScrollTo(index) { + itemScrollController.scrollTo( + index: index, + duration: const Duration(milliseconds: 10), + ); + } + @override void onClose() { if (super.watchData.value != null) { @@ -141,10 +192,12 @@ class NovelController extends ReaderController { ); } SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + flutterTts.stop(); mouseTimer?.cancel(); super.onClose(); } + //獲取目標章節內容,但不更新當前頁面 @override Future loadTargetContent(int targetIndex) async { try { @@ -155,6 +208,7 @@ class NovelController extends ReaderController { await runtime.watch(playList[targetIndex].url); items[targetIndex] = updatedData.content as List; itemlength[targetIndex] = updatedData.content.length; + subtitles[targetIndex] = updatedData.subtitle ?? ''; } catch (e) { error.value = e.toString(); } @@ -166,6 +220,7 @@ class NovelController extends ReaderController { return; } + // 加載上一章節,並跳轉到剛才的位置 @override Future loadPrevChapter() async { await loadTargetContent(index.value - 1); @@ -185,6 +240,7 @@ class NovelController extends ReaderController { await runtime.watch(cuurentPlayUrl) as ExtensionFikushonWatch; itemlength[index.value] = (watchData.value as dynamic)?.content.length; items[index.value] = (watchData.value as dynamic)?.content; + subtitles[index.value] = (watchData.value as dynamic)?.subtitle ?? ''; } catch (e) { error.value = e.toString(); } diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index 0cec1776..5a3b6ab0 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -45,7 +45,6 @@ abstract class ReaderController extends GetxController { final isScrolled = true.obs; final updateSlider = true.obs; final isInfinityScrollMode = false.obs; - final isLoading = false.obs; Timer? _barreryTimer; //點擊區域是否反轉 final RxBool tapRegionIsReversed = false.obs; @@ -62,7 +61,7 @@ abstract class ReaderController extends GetxController { final RxDouble nextPageHitBox = 0.3.obs; final RxDouble prevPageHitBox = 0.3.obs; final enableAutoScroll = false.obs; - final height = 1000.0.obs; + // final height = 1000.0.obs; final RxBool isMouseHover = false.obs; final RxBool setControllPanel = false.obs; Timer? mouseTimer; @@ -102,26 +101,7 @@ abstract class ReaderController extends GetxController { await _statusBar(); _barreryTimer = Timer.periodic(const Duration(seconds: 10), (timer) => _statusBar()); - // ever(index, (callback) { - // getContent(); - // }); - ever(enableAutoScroll, (callback) { - if (callback) { - autoScrollTimer = Timer.periodic( - Duration(milliseconds: autoScrollInterval.value), (timer) { - if (isScrolled.value) { - scrollOffsetController.animateScroll( - duration: const Duration(milliseconds: 100), - curve: Curves.ease, - offset: autoScrollOffset.value, - ); - } - }); - return; - } - autoScrollTimer?.cancel(); - }); mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { if (setControllPanel.value) { isShowControlPanel.value = true; @@ -141,16 +121,20 @@ abstract class ReaderController extends GetxController { return progress; } - int globalToLocalProgress(int globalProgress) { - int progress = globalProgress; - for (int i = 0; i < index.value; i++) { - if (globalProgress < itemlength[i]) { + List globalToLocalProgress(int globalProgress) { + int fullIndex = 0; + int localProgress = 0; + int chapter = 0; + // debugPrint(currentLocalProgress.value.toString()); + for (int i = 0; i < itemlength.length; i++) { + fullIndex += itemlength[i]; + if (fullIndex > globalProgress) { + chapter = i; + localProgress = globalProgress - (fullIndex - itemlength[i]); break; } - globalProgress -= itemlength[i]; } - debugPrint(progress.toString()); - return progress; + return [localProgress, chapter]; } void previousPage() {} diff --git a/lib/utils/color.dart b/lib/utils/color.dart index faaf6d8f..34c14585 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -19,4 +19,17 @@ class ColorUtils { ][colorIndex]; return color!; } + + static List baseColors = [ + Colors.white, + Colors.black, + Colors.red, + Colors.orange, + Colors.yellow, + Colors.green, + Colors.cyan, + Colors.blue, + Colors.purple, + Colors.transparent, + ]; } diff --git a/lib/utils/miru_storage.dart b/lib/utils/miru_storage.dart index 06978515..baa2bad0 100644 --- a/lib/utils/miru_storage.dart +++ b/lib/utils/miru_storage.dart @@ -134,6 +134,11 @@ class MiruStorage { await _initSetting(SettingKey.prevPageHitBox, 0.2); await _initSetting(SettingKey.autoScrollInterval, 300); await _initSetting(SettingKey.autoScrollOffset, 20.0); + await _initSetting( + SettingKey.ttsLanguage, Platform.localeName.split('_')[0]); + await _initSetting(SettingKey.ttsPitch, 0.3); + await _initSetting(SettingKey.ttsRate, 0.3); + await _initSetting(SettingKey.ttsVolume, 0.5); } static _initSetting(String key, dynamic value) async { @@ -198,4 +203,8 @@ class SettingKey { static String prevPageHitBox = "PrevPageHitBox"; static String autoScrollInterval = "AutoScrollInterval"; static String autoScrollOffset = "AutoScrollOffset"; + static String ttsLanguage = "TTSLanguage"; + static String ttsPitch = "TTSPitch"; + static String ttsRate = "TTSRate"; + static String ttsVolume = "TTSVolume"; } diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 844d4ba3..98bfa759 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -369,7 +369,7 @@ class _ComicReaderContentState extends State { @override Widget build(BuildContext context) { - _c.height.value = MediaQuery.of(context).size.height; + // _c.height.value = MediaQuery.of(context).size.height; return PlatformBuildWidget( androidBuilder: (context) { return Scaffold( diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index 48cebea9..dabb1b15 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -73,9 +73,6 @@ class _NovelReaderContentState extends State { if (_c.watchData.value == null) { return const Center(child: ProgressRing()); } - - final watchData = _c.watchData.value!; - final listviewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; @@ -108,7 +105,8 @@ class _NovelReaderContentState extends State { vertical: 16, ), itemBuilder: (context, index) { - if (_c.globalToLocalProgress(index) == 0) { + final localProgress = _c.globalToLocalProgress(index); + if (localProgress[0] == 0) { return Column(children: [ const SizedBox( height: 20, @@ -116,15 +114,16 @@ class _NovelReaderContentState extends State { Padding( padding: const EdgeInsets.only(bottom: 20), child: Text( - _c.title + _c.playList[_c.index.value].name, + _c.title + _c.playList[localProgress[1]].name, style: const TextStyle(fontSize: 26), ), ), - if (watchData.subtitle != null) ...[ + if (_c + .subtitles[localProgress[1]].isNotEmpty) ...[ Padding( padding: const EdgeInsets.only(bottom: 20), child: Text( - watchData.subtitle!, + _c.subtitles[localProgress[1]], style: const TextStyle(fontSize: 20), ), ) @@ -186,7 +185,7 @@ class _NovelReaderContentState extends State { Widget _textContent(int index, double fontSize) { final content = _c.items.expand((element) => element).toList(); - return Padding( + return Obx(() => Padding( padding: const EdgeInsets.only(bottom: 20), child: SelectableText.rich( onTap: () { @@ -198,6 +197,7 @@ class _NovelReaderContentState extends State { TextSpan( text: content[index], style: TextStyle( + color: _c.textColor.value, fontSize: fontSize, fontWeight: FontWeight.w400, height: 2, @@ -207,7 +207,7 @@ class _NovelReaderContentState extends State { ), ], ), - )); + ))); // Obx(() => Column( // children: [ // _c.enableSelectText.value diff --git a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart index e7b76069..45f28dbd 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart @@ -2,6 +2,7 @@ import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:miru_app/controllers/watch/novel_controller.dart'; +import 'package:miru_app/utils/color.dart'; import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/utils/miru_storage.dart'; @@ -20,37 +21,247 @@ class _NovelReaderSettingsState extends State { late final NovelController _c = Get.find(tag: widget.tag); Widget _buildAndroid(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16), - child: Obx(() => Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text("novel-settings.font-size".i18n), - const SizedBox(height: 16), - Slider( - value: _c.fontSize.value, - label: _c.fontSize.value.toString(), - onChanged: (value) { - _c.fontSize.value = value; - }, - divisions: 12, - min: 12, - max: 24, + return DefaultTabController( + length: 3, + child: Column( + children: [ + TabBar(tabs: [ + Tab( + text: "common.common".i18n, ), - const SizedBox(height: 16), - SettingsSwitchTile( - icon: const Icon(Icons.coffee), - title: "reader-settings.enable-wakelock".i18n, - buildValue: () => - MiruStorage.getSetting(SettingKey.enableWakelock), - onChanged: (val) { - WakelockPlus.toggle(enable: val); - MiruStorage.setSetting(SettingKey.enableWakelock, val); - }) - ], - )), - ); + Tab(text: "common.tts".i18n), + Tab( + text: "settings.theme".i18n, + ) + ]), + SizedBox( + height: MediaQuery.of(context).size.height, + child: TabBarView(children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 阅读模式 + const SizedBox(height: 16), + Text('reader-settings.indicator-alignment'.i18n), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: [ + ButtonSegment( + value: Alignment.bottomLeft, + label: Transform.rotate( + angle: -3.14, + child: const Icon(Icons.arrow_outward)), + ), + ButtonSegment( + value: Alignment.bottomRight, + label: Transform.rotate( + angle: 1.57, + child: const Icon(Icons.arrow_outward)), + ), + ButtonSegment( + value: Alignment.topLeft, + label: Transform.rotate( + angle: -1.57, + child: const Icon(Icons.arrow_outward)), + ), + const ButtonSegment( + value: Alignment.topRight, + label: Icon(Icons.arrow_outward), + ) + ], + selected: {_c.alignMode.value}, + onSelectionChanged: (value) { + if (value.isNotEmpty) { + _c.alignMode.value = value.first; + } + }, + showSelectedIcon: false, + ), + ), + const SizedBox(height: 16), + Text('comic-settings.status-bar'.i18n), + const SizedBox(height: 5), + Wrap( + spacing: 5, + children: _c.statusBarElement.keys + .map((e) => FilterChip( + label: Text(e), + selected: _c.statusBarElement[e]!.value, + onSelected: (val) { + _c.statusBarElement[e]!.value = val; + })) + .toList(), + ), + + SettingsSwitchTile( + icon: const Icon(Icons.coffee), + title: "reader-settings.enable-wakelock".i18n, + buildValue: () => _c.enableWakeLock.value, + onChanged: (val) { + WakelockPlus.toggle(enable: val); + _c.enableWakeLock.value = val; + MiruStorage.setSetting( + SettingKey.enableWakelock, val); + }), + const SizedBox(height: 16), + Text('novel-settings.font-size'.i18n), + const SizedBox(height: 5), + SizedBox( + width: double.infinity, + child: Slider( + value: _c.fontSize.value, + onChanged: (value) { + _c.fontSize.value = value; + }, + label: _c.fontSize.value.toString(), + divisions: 24, + min: 12, + max: 24, + )) + ], + )), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsSwitchTile( + icon: const Icon(Icons.play_arrow_rounded), + title: "novel-settings.enable-tts".i18n, + buildValue: () => _c.enableAutoScroll.value, + onChanged: (val) { + Get.back(); + _c.enableAutoScroll.value = val; + }), + const SizedBox(height: 16), + // Text('novel-settings.ttslang'.i18n), + // const SizedBox(height: 5), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text('novel-settings.tts-lang'.i18n), + DropdownMenu( + initialSelection: _c.ttsLangValue.value, + dropdownMenuEntries: _c.ttsLang + .map>( + (element) { + return DropdownMenuEntry( + value: element, + label: element.toString(), + ); + }).toList(), + onSelected: (String? newValue) { + if (newValue != null) { + _c.ttsLangValue.value = newValue; + MiruStorage.setSetting( + SettingKey.ttsLanguage, newValue); + } + }, + ) + ]), + const SizedBox(height: 16), + Text('novel-settings.tts-rate'.i18n), + const SizedBox(height: 5), + SizedBox( + width: double.infinity, + child: Slider( + value: _c.ttsRate.value, + onChanged: (value) { + _c.ttsRate.value = value; + MiruStorage.setSetting( + SettingKey.ttsRate, value); + }, + min: 0, + max: 1, + divisions: 20, + label: _c.ttsRate.value.toStringAsFixed(2), + ), + ), + const SizedBox(height: 16), + Text('novel-settings.tts-volume'.i18n), + const SizedBox(height: 5), + SizedBox( + width: double.infinity, + child: Slider( + value: _c.ttsVolume.value, + onChanged: (value) { + _c.ttsVolume.value = value; + MiruStorage.setSetting( + SettingKey.ttsVolume, value); + }, + min: 0, + max: 1, + divisions: 20, + label: _c.ttsVolume.value.toStringAsFixed(2), + ), + ), + const SizedBox(height: 16), + Text('novel-settings.tts-pitch'.i18n), + const SizedBox(height: 5), + SizedBox( + width: double.infinity, + child: Slider( + value: _c.ttsPitch.value, + onChanged: (value) { + _c.ttsPitch.value = value; + MiruStorage.setSetting( + SettingKey.ttsPitch, value); + }, + label: _c.ttsPitch.value.toStringAsFixed(2), + min: 0.5, + max: 2, + divisions: 30, + ), + ), + ], + )), + ), + Padding( + padding: const EdgeInsets.all(16), + child: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("novel-settings.text-color".i18n), + const SizedBox(height: 5), + Wrap( + spacing: 5, + children: List.generate( + ColorUtils.baseColors.length, + (index) => ChoiceChip( + onSelected: (val) { + if (val) { + _c.textColor.value = + ColorUtils.baseColors[index]; + // MiruStorage.setSetting( + // SettingKey.textColor, + // ColorUtils.baseColors[index]); + } + }, + label: Container( + width: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorUtils.baseColors[index], + ), + ), + selected: ColorUtils.baseColors[index] == + _c.textColor.value)), + ), + ], + )), + ) + ]), + ) + ], + )); } Widget _buildDesktop(BuildContext context) { diff --git a/lib/views/widgets/watch/control_panel_header.dart b/lib/views/widgets/watch/control_panel_header.dart index 42881cf4..f0cfb113 100644 --- a/lib/views/widgets/watch/control_panel_header.dart +++ b/lib/views/widgets/watch/control_panel_header.dart @@ -36,6 +36,13 @@ class _ControlPanelHeaderState child: AppBar( title: Text(_c.title), actions: [ + IconButton( + onPressed: () { + _c.enableAutoScroll.value = !_c.enableAutoScroll.value; + }, + icon: _c.enableAutoScroll.value + ? const Icon(Icons.stop_rounded) + : const Icon(Icons.play_arrow_rounded)), IconButton( onPressed: () { showModalBottomSheet( diff --git a/lib/views/widgets/watch/reader_view.dart b/lib/views/widgets/watch/reader_view.dart index 4810bf33..49f4421f 100644 --- a/lib/views/widgets/watch/reader_view.dart +++ b/lib/views/widgets/watch/reader_view.dart @@ -87,7 +87,7 @@ class ReaderView extends StatelessWidget { // ), , - if (c.isShowControlPanel.value) ...[ + if (c.isShowControlPanel.value || c.enableAutoScroll.value) ...[ // 顶部控制 Positioned( child: ControlPanelHeader( @@ -98,12 +98,6 @@ class ReaderView extends StatelessWidget { // 底部控制 ], ControlPanelFooter(tag), - if (c.enableAutoScroll.value && Platform.isAndroid) - ElevatedButton( - onPressed: () { - c.enableAutoScroll.value = false; - }, - child: const Icon(Icons.stop)), ], ), ); From bb4be5fef199d6509cd465d66c9e49550cb3864c Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 11 Feb 2024 01:45:29 +0800 Subject: [PATCH 14/24] add color settings in mobile novel reader --- assets/i18n/en.json | 5 +- lib/controllers/watch/novel_controller.dart | 11 +++- lib/utils/miru_storage.dart | 4 +- .../reader/novel/novel_reader_content.dart | 9 ++- .../reader/novel/novel_reader_settings.dart | 66 ++++++++++++++++++- 5 files changed, 89 insertions(+), 6 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index b405dfe6..4efac953 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -215,7 +215,10 @@ "tts-lang": "TTS Language", "tts-volume": "TTS Volume", "tts-pitch": "TTS Pitch", - "text-color": "Text Color" + "text-color": "Text Color", + "heighlight-color": "Highlight Color", + "heighlight-text-color": "Highlight Text Color", + "leading": "Leading" }, "bugreport": { "auto-remove-subtitle": "delete in ~ days", diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index f1e6ee8d..e410ff30 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -38,6 +38,10 @@ class NovelController extends ReaderController { late final RxList subtitles = List.generate(playList.length, (index) => "").obs; final Rx textColor = Colors.white.obs; + final Rx heighLightColor = Colors.blue.obs; + final Rx heighLightTextColor = Colors.white.obs; + final RxInt currentLine = 0.obs; + final RxDouble leading = 20.0.obs; initTts() { ttsVolume.value = MiruStorage.getSetting(SettingKey.ttsVolume); ttsRate.value = MiruStorage.getSetting(SettingKey.ttsRate); @@ -60,6 +64,7 @@ class NovelController extends ReaderController { WakelockPlus.toggle( enable: MiruStorage.getSetting(SettingKey.enableWakelock)); ttsLangValue.value = MiruStorage.getSetting(SettingKey.ttsLanguage); + leading.value = MiruStorage.getSetting(SettingKey.leading); ttsLang.value = await flutterTts.getLanguages; debugPrint(ttsLang.toString()); itemPositionsListener.itemPositions.addListener(() { @@ -122,10 +127,14 @@ class NovelController extends ReaderController { } final readingProgress = items[index.value][i]; debugPrint("current reading: $readingProgress , progress: $i"); - animeScrollTo(localToGloabalProgress(i) - 1); + animeScrollTo((localToGloabalProgress(i) - 2) > 0 + ? localToGloabalProgress(i) - 2 + : 0); + currentLine.value = i; await flutterTts.speak(items[index.value][i]); } enableAutoScroll.value = false; + currentLine.value = -1; }); ever(currentGlobalProgress, (callback) { if (updateSlider.value) { diff --git a/lib/utils/miru_storage.dart b/lib/utils/miru_storage.dart index baa2bad0..34b5e743 100644 --- a/lib/utils/miru_storage.dart +++ b/lib/utils/miru_storage.dart @@ -136,9 +136,10 @@ class MiruStorage { await _initSetting(SettingKey.autoScrollOffset, 20.0); await _initSetting( SettingKey.ttsLanguage, Platform.localeName.split('_')[0]); - await _initSetting(SettingKey.ttsPitch, 0.3); + await _initSetting(SettingKey.ttsPitch, 1.0); await _initSetting(SettingKey.ttsRate, 0.3); await _initSetting(SettingKey.ttsVolume, 0.5); + await _initSetting(SettingKey.leading, 20.0); } static _initSetting(String key, dynamic value) async { @@ -207,4 +208,5 @@ class SettingKey { static String ttsPitch = "TTSPitch"; static String ttsRate = "TTSRate"; static String ttsVolume = "TTSVolume"; + static String leading = "Leading"; } diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index dabb1b15..60bd57a2 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -186,7 +186,7 @@ class _NovelReaderContentState extends State { Widget _textContent(int index, double fontSize) { final content = _c.items.expand((element) => element).toList(); return Obx(() => Padding( - padding: const EdgeInsets.only(bottom: 20), + padding: EdgeInsets.only(bottom: _c.leading.value), child: SelectableText.rich( onTap: () { _c.setControllPanel.value = !_c.setControllPanel.value; @@ -197,9 +197,14 @@ class _NovelReaderContentState extends State { TextSpan( text: content[index], style: TextStyle( - color: _c.textColor.value, + color: index == _c.currentLine.value + ? _c.heighLightTextColor.value + : _c.textColor.value, fontSize: fontSize, fontWeight: FontWeight.w400, + backgroundColor: index == _c.currentLine.value + ? _c.heighLightColor.value + : null, height: 2, textBaseline: TextBaseline.ideographic, fontFamily: 'Microsoft Yahei', diff --git a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart index 45f28dbd..240783f3 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart @@ -122,7 +122,23 @@ class _NovelReaderSettingsState extends State { divisions: 24, min: 12, max: 24, - )) + )), + const SizedBox(height: 16), + Text("novel-settings.leading".i18n), + const SizedBox(height: 5), + SizedBox( + width: double.infinity, + child: Slider( + value: _c.leading.value, + onChanged: (value) { + _c.leading.value = value; + }, + label: _c.leading.value.toString(), + divisions: 40, + min: 0, + max: 40, + ), + ), ], )), ), @@ -255,6 +271,54 @@ class _NovelReaderSettingsState extends State { selected: ColorUtils.baseColors[index] == _c.textColor.value)), ), + const SizedBox(height: 16), + Text("novel-settings.heighlight-color".i18n), + const SizedBox(height: 5), + Wrap( + spacing: 5, + children: List.generate( + ColorUtils.baseColors.length, + (index) => ChoiceChip( + onSelected: (val) { + if (val) { + _c.heighLightColor.value = + ColorUtils.baseColors[index]; + } + }, + label: Container( + width: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorUtils.baseColors[index], + ), + ), + selected: ColorUtils.baseColors[index] == + _c.heighLightColor.value)), + ), + const SizedBox(height: 16), + Text("novel-settings.heighlight-text-color".i18n), + const SizedBox(height: 5), + Wrap( + spacing: 5, + children: List.generate( + ColorUtils.baseColors.length, + (index) => ChoiceChip( + onSelected: (val) { + if (val) { + _c.heighLightTextColor.value = + ColorUtils.baseColors[index]; + } + }, + label: Container( + width: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorUtils.baseColors[index], + ), + ), + selected: ColorUtils.baseColors[index] == + _c.heighLightTextColor.value)), + ), ], )), ) From bf1a99ef69e89e317d4eff1b1d0108e2e87a2452 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Wed, 14 Feb 2024 15:52:46 +0800 Subject: [PATCH 15/24] add single page mode for novel reader *Note slider is not working --- lib/controllers/watch/novel_controller.dart | 51 ++- lib/controllers/watch/reader_controller.dart | 20 + lib/utils/miru_storage.dart | 2 +- .../reader/novel/novel_reader_content.dart | 347 ++++++++++++------ .../reader/novel/novel_reader_settings.dart | 34 +- .../widgets/watch/control_panel_header.dart | 124 ++++--- lib/views/widgets/watch/playlist.dart | 36 +- lib/views/widgets/watch/reader_view.dart | 2 +- pubspec.lock | 10 +- pubspec.yaml | 2 + 10 files changed, 436 insertions(+), 192 deletions(-) diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index e410ff30..c15ca4a3 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:bookfx/bookfx.dart'; import 'package:get/get.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; @@ -11,6 +12,12 @@ import 'package:flutter/services.dart'; import 'package:flutter_tts/flutter_tts.dart'; import 'package:flutter/material.dart'; +enum NovelReadMode { + singlePage, + doublePage, + scroll, +} + class NovelController extends ReaderController { NovelController({ required super.title, @@ -42,11 +49,24 @@ class NovelController extends ReaderController { final Rx heighLightTextColor = Colors.white.obs; final RxInt currentLine = 0.obs; final RxDouble leading = 20.0.obs; - initTts() { + final RxInt bookPage = (-1).obs; + final RxInt totalBookPage = 0.obs; + Map readmode = { + 'singlePage': NovelReadMode.singlePage, + 'doublePage': NovelReadMode.doublePage, + 'scroll': NovelReadMode.scroll, + }; + final readType = NovelReadMode.scroll.obs; + // final Rx bookController = BookController().obs; + final bookController = BookController(); + initTts() async { + ttsLangValue.value = MiruStorage.getSetting(SettingKey.ttsLanguage); ttsVolume.value = MiruStorage.getSetting(SettingKey.ttsVolume); ttsRate.value = MiruStorage.getSetting(SettingKey.ttsRate); ttsPitch.value = MiruStorage.getSetting(SettingKey.ttsPitch); flutterTts = FlutterTts(); + ttsLang.value = await flutterTts.getLanguages; + debugPrint(ttsLang.toString()); flutterTts.awaitSpeakCompletion(true); flutterTts.setCompletionHandler(() { debugPrint("completed"); @@ -57,16 +77,14 @@ class NovelController extends ReaderController { void onInit() async { super.onInit(); getContent(); - initTts(); + await initTts(); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); fontSize.value = MiruStorage.getSetting(SettingKey.novelFontSize); + leading.value = MiruStorage.getSetting(SettingKey.leading); // textColor.value = MiruStorage.getSetting(SettingKey.textColor); WakelockPlus.toggle( enable: MiruStorage.getSetting(SettingKey.enableWakelock)); - ttsLangValue.value = MiruStorage.getSetting(SettingKey.ttsLanguage); - leading.value = MiruStorage.getSetting(SettingKey.leading); - ttsLang.value = await flutterTts.getLanguages; - debugPrint(ttsLang.toString()); + itemPositionsListener.itemPositions.addListener(() { if (itemPositionsListener.itemPositions.value.isEmpty) { return; @@ -78,11 +96,18 @@ class NovelController extends ReaderController { enableSelectText.value = false; hideControlPanel(); }); - ever( - fontSize, - (callback) => MiruStorage.setSetting(SettingKey.novelFontSize, callback), - ); + ever(readType, (callback) { + if (callback == NovelReadMode.scroll) { + bookPage.value = -1; + } + }); + ever(bookPage, (callback) { + if (callback == -1) { + return; + } + //只處理單頁模式 + }); // 切换章节时重置页码 ever(super.watchData, (callback) async { if (isRecover.value || callback == null) { @@ -136,6 +161,7 @@ class NovelController extends ReaderController { enableAutoScroll.value = false; currentLine.value = -1; }); + ever(currentGlobalProgress, (callback) { if (updateSlider.value) { progress.value = callback; @@ -203,6 +229,7 @@ class NovelController extends ReaderController { SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); flutterTts.stop(); mouseTimer?.cancel(); + bookController.dispose(); super.onClose(); } @@ -223,6 +250,10 @@ class NovelController extends ReaderController { } } + void setReadingPage(int page) { + bookPage.value = page; + } + @override Future loadNextChapter() async { await loadTargetContent(index.value + 1); diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index 5a3b6ab0..8261424e 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -9,6 +9,7 @@ import 'package:miru_app/utils/miru_storage.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:miru_app/utils/i18n.dart'; import 'package:battery_plus/battery_plus.dart'; +import 'package:screen_brightness/screen_brightness.dart'; abstract class ReaderController extends GetxController { final String title; @@ -72,6 +73,7 @@ abstract class ReaderController extends GetxController { late final List itemlength = List.filled(playList.length, 0); final currentGlobalProgress = 0.obs; final currentLocalProgress = 0.obs; + final RxBool enableTapRegion = true.obs; final statusBarElement = { 'reader-settings.battery'.i18n: true.obs, 'reader-settings.time'.i18n: true.obs, @@ -90,6 +92,7 @@ abstract class ReaderController extends GetxController { } final alignMode = Alignment.bottomLeft.obs; + final RxDouble brightness = 0.5.obs; @override void onInit() async { @@ -99,6 +102,7 @@ abstract class ReaderController extends GetxController { nextPageHitBox.value = _nextPageHitBox; prevPageHitBox.value = _prevPageHitBox; await _statusBar(); + await _currentBrightness(); _barreryTimer = Timer.periodic(const Duration(seconds: 10), (timer) => _statusBar()); @@ -112,6 +116,22 @@ abstract class ReaderController extends GetxController { super.onInit(); } + Future _currentBrightness() async { + try { + brightness.value = await ScreenBrightness().current; + } catch (e) { + throw 'Failed to get current brightness'; + } + } + + Future setBrightness(double brightness) async { + try { + await ScreenBrightness().setScreenBrightness(brightness); + } catch (e) { + throw 'Failed to set brightness'; + } + } + int localToGloabalProgress(int localProgress) { int progress = 0; for (int i = 0; i < index.value; i++) { diff --git a/lib/utils/miru_storage.dart b/lib/utils/miru_storage.dart index 34b5e743..285ed4a1 100644 --- a/lib/utils/miru_storage.dart +++ b/lib/utils/miru_storage.dart @@ -139,7 +139,7 @@ class MiruStorage { await _initSetting(SettingKey.ttsPitch, 1.0); await _initSetting(SettingKey.ttsRate, 0.3); await _initSetting(SettingKey.ttsVolume, 0.5); - await _initSetting(SettingKey.leading, 20.0); + await _initSetting(SettingKey.leading, 2.0); } static _initSetting(String key, dynamic value) async { diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index 60bd57a2..aa7cfd03 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -8,6 +8,7 @@ import 'package:miru_app/views/widgets/progress.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:based_battery_indicator/based_battery_indicator.dart'; +import 'package:bookfx/bookfx.dart'; class NovelReaderContent extends StatefulWidget { const NovelReaderContent(this.tag, {super.key}); @@ -19,6 +20,10 @@ class NovelReaderContent extends StatefulWidget { class _NovelReaderContentState extends State { late final _c = Get.find(tag: widget.tag); + // final _controller = GlobalKey(); + final RxList> singlePageText = >[].obs; + final List line = []; + late int totalPage = singlePageText.length; Widget _buildDisplay(Widget child) { if (_c.statusBarElement.values.every((element) => element.value == false)) { return child; @@ -50,7 +55,7 @@ class _NovelReaderContentState extends State { // // 宽度 大于 800 就是整体宽度的一半 final maxWidth = constraints.maxWidth; // final width = maxWidth > 800 ? maxWidth / 2 : maxWidth; - // final height = constraints.maxHeight; + final height = constraints.maxHeight; if (_c.error.value.isNotEmpty) { return SizedBox( width: double.infinity, @@ -77,65 +82,167 @@ class _NovelReaderContentState extends State { maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; final fontSize = _c.fontSize.value; - - return Center( - child: NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.atEdge) { - bool isTop = metrics.pixels <= 0; - if (isTop) { - debugPrint('At the top'); - _c.loadPrevChapter(); - } else { - debugPrint('At the bottom'); - _c.loadNextChapter(); + final leading = _c.leading.value; + if (_c.readType.value == NovelReadMode.scroll) { + return Center( + child: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels <= 0; + if (isTop) { + debugPrint('At the top'); + _c.loadPrevChapter(); + } else { + debugPrint('At the bottom'); + _c.loadNextChapter(); + } } - } - return true; - }, - child: ScrollablePositionedList.builder( - itemPositionsListener: _c.itemPositionsListener, - initialScrollIndex: _c.currentGlobalProgress.value, - itemScrollController: _c.itemScrollController, - scrollOffsetController: _c.scrollOffsetController, - padding: EdgeInsets.symmetric( - horizontal: listviewPadding, - vertical: 16, - ), - itemBuilder: (context, index) { - final localProgress = _c.globalToLocalProgress(index); - if (localProgress[0] == 0) { - return Column(children: [ - const SizedBox( - height: 20, - ), - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text( + return true; + }, + child: ScrollablePositionedList.builder( + itemPositionsListener: _c.itemPositionsListener, + initialScrollIndex: _c.currentGlobalProgress.value, + itemScrollController: _c.itemScrollController, + scrollOffsetController: _c.scrollOffsetController, + padding: EdgeInsets.symmetric( + horizontal: listviewPadding, + vertical: 16, + ), + itemBuilder: (context, index) { + final localProgress = + _c.globalToLocalProgress(index); + if (localProgress[0] == 0) { + return Column(children: [ + const SizedBox( + height: 20, + ), + Text( _c.title + _c.playList[localProgress[1]].name, style: const TextStyle(fontSize: 26), ), - ), - if (_c - .subtitles[localProgress[1]].isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.only(bottom: 20), - child: Text( + const SizedBox( + height: 20, + ), + if (_c.subtitles[localProgress[1]] + .isNotEmpty) ...[ + Text( _c.subtitles[localProgress[1]], style: const TextStyle(fontSize: 20), ), - ) - ], - _textContent(index, fontSize) - ]); - } - return _textContent(index, fontSize); - }, - itemCount: _c.items.expand((element) => element).length, - )), - ); + const SizedBox( + height: 20, + ) + ], + _textContent(index, fontSize, leading) + ]); + } + return _textContent(index, fontSize, leading); + }, + itemCount: + _c.items.expand((element) => element).length, + )), + ); + } + + List dimensions = [ + const PlaceholderDimensions( + size: Size(40, 0), //widget span size + alignment: PlaceholderAlignment.bottom, + ) + ]; + double heightSum = 0; + line.clear(); + singlePageText.clear(); + for (int index = 0; + index < _c.items.expand((element) => element).length; + index++) { + final textPainter = TextPainter( + text: _text(index, fontSize), + textDirection: TextDirection.ltr, + ) + ..setPlaceholderDimensions(dimensions) + ..layout(maxWidth: maxWidth / 2); + line.add(_textContent(index, fontSize, leading)); + //處理超出高度的情況 + if (heightSum + textPainter.size.height + leading > height) { + if (index > _c.currentGlobalProgress.value) { + // _c.bookPage.value = index; + } + singlePageText.add(List.from(line)); + line.clear(); + heightSum = 0; + continue; + } + heightSum += (textPainter.size.height + leading); + // debugPrint("${textPainter.size.height} $height $heightSum"); + } + if (line.isNotEmpty) { + singlePageText.add(List.from(line)); + } + totalPage = singlePageText.length; + debugPrint(singlePageText.length.toString()); + //old page flip + // return Obx(() => PageFlipWidget( + // key: _controller, + // backgroundColor: + // Theme.of(context).scaffoldBackgroundColor, + // children: singlePageText, + // )); + //pageview + // return Obx(() => PageView.builder( + // itemBuilder: (context, index) { + // return Padding( + // padding: EdgeInsets.symmetric( + // horizontal: listviewPadding, vertical: 16), + // child: Column( + // children: singlePageText[index], + // ), + // ); + // }, + // itemCount: singlePageText.length, + // )); + return BookFx( + pageCount: singlePageText.length, + currentBgColor: Colors.black, + size: Size(maxWidth, height), + lastCallBack: (val) { + _c.setReadingPage(val); + }, + nextCallBack: (val) { + _c.setReadingPage(val); + }, + currentPage: (index) { + if (index > singlePageText.length) { + _c.bookController.goTo(singlePageText.length - 1); + return Container(); + } + return Container( + color: Theme.of(context).scaffoldBackgroundColor, + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: listviewPadding, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: singlePageText[index]), + )); + }, + nextPage: (index) { + //處理頁數到底的情況 + if (index > singlePageText.length) { + _c.bookController.goTo(singlePageText.length - 1); + return Container(); + } + + return Padding( + padding: EdgeInsets.symmetric( + horizontal: listviewPadding, vertical: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: singlePageText[index])); + }, + controller: _c.bookController); }, ), )) @@ -145,13 +252,27 @@ class _NovelReaderContentState extends State { Widget _indicatorBuilder() { return Obx(() => Row(mainAxisSize: MainAxisSize.min, children: [ if (_c.statusBarElement["reader-settings.page-indicator".i18n]! - .value) ...[ + .value && + _c.readType.value == NovelReadMode.scroll) ...[ Text( "${_c.currentLocalProgress.value + 1} ${"novel-settings.line".i18n}", style: const TextStyle(color: Colors.white, fontSize: 15), ), const SizedBox(width: 8) ], + if (_c.statusBarElement["reader-settings.page-indicator".i18n]! + .value && + _c.readType.value != NovelReadMode.scroll) ...[ + ListenableBuilder( + listenable: _c.bookController, + builder: (context, child) { + return Text( + "${_c.bookController.currentIndex + 1}/$totalPage", + style: + const TextStyle(color: Colors.white, fontSize: 15)); + }), + const SizedBox(width: 8) + ], if (_c.statusBarElement["reader-settings.battery-icon".i18n]! .value) ...[ BasedBatteryIndicator( @@ -183,78 +304,60 @@ class _NovelReaderContentState extends State { ])); } - Widget _textContent(int index, double fontSize) { + TextSpan _text(int index, double fontSize) { final content = _c.items.expand((element) => element).toList(); - return Obx(() => Padding( - padding: EdgeInsets.only(bottom: _c.leading.value), - child: SelectableText.rich( - onTap: () { - _c.setControllPanel.value = !_c.setControllPanel.value; - }, + return TextSpan( + children: [ + const WidgetSpan(child: SizedBox(width: 40.0)), + TextSpan( + text: content[index], + style: TextStyle( + color: index == _c.currentLine.value + ? _c.heighLightTextColor.value + : _c.textColor.value, + fontSize: fontSize, + fontWeight: FontWeight.w400, + backgroundColor: + index == _c.currentLine.value ? _c.heighLightColor.value : null, + height: 2, + textBaseline: TextBaseline.ideographic, + fontFamily: 'Microsoft Yahei', + ), + ), + ], + ); + } + + Widget _textContent(int index, double fontSize, double leading) { + final content = _c.items.expand((element) => element).toList(); + + return SelectableText.rich( + // key: globalKeys[index], + onTap: () { + _c.setControllPanel.value = !_c.setControllPanel.value; + }, + TextSpan( + children: [ + const WidgetSpan(child: SizedBox(width: 40.0)), TextSpan( - children: [ - const WidgetSpan(child: SizedBox(width: 40.0)), - TextSpan( - text: content[index], - style: TextStyle( - color: index == _c.currentLine.value - ? _c.heighLightTextColor.value - : _c.textColor.value, - fontSize: fontSize, - fontWeight: FontWeight.w400, - backgroundColor: index == _c.currentLine.value - ? _c.heighLightColor.value - : null, - height: 2, - textBaseline: TextBaseline.ideographic, - fontFamily: 'Microsoft Yahei', - ), - ), - ], + text: content[index], + style: TextStyle( + color: index == _c.currentLine.value + ? _c.heighLightTextColor.value + : _c.textColor.value, + fontSize: fontSize, + fontWeight: FontWeight.w400, + backgroundColor: index == _c.currentLine.value + ? _c.heighLightColor.value + : null, + height: leading, + textBaseline: TextBaseline.ideographic, + fontFamily: 'Microsoft Yahei', + ), ), - ))); - // Obx(() => Column( - // children: [ - // _c.enableSelectText.value - // ? SelectableText.rich( - // onTap: () { - // _c.setControllPanel.value = - // !_c.setControllPanel.value; - // }, - // TextSpan( - // children: [ - // const WidgetSpan(child: SizedBox(width: 40.0)), - // TextSpan( - // text: content[index], - // style: TextStyle( - // fontSize: fontSize, - // fontWeight: FontWeight.w400, - // height: 2, - // textBaseline: TextBaseline.ideographic, - // fontFamily: 'Microsoft Yahei', - // ), - // ), - // ], - // ), - // ) - // : GestureDetector( - // onTap: () { - // _c.setControllPanel.value = - // !_c.setControllPanel.value; - // _c.enableSelectText.value = true; - // }, - // child: Text( - // content[index], - // style: TextStyle( - // fontSize: fontSize, - // fontWeight: FontWeight.w400, - // height: 2, - // textBaseline: TextBaseline.ideographic, - // fontFamily: 'Microsoft Yahei', - // ), - // )) - // ], - // ))); + ], + ), + ); } Widget _buildAndroid(BuildContext context) { diff --git a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart index 240783f3..65195404 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart @@ -44,6 +44,34 @@ class _NovelReaderSettingsState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ // 阅读模式 + Text('reader-settings.read-mode'.i18n), + const SizedBox(height: 5), + SizedBox( + width: double.infinity, + child: SegmentedButton( + segments: [ + ButtonSegment( + value: NovelReadMode.scroll, + label: Text('novel-settings.scroll'.i18n), + ), + ButtonSegment( + value: NovelReadMode.singlePage, + label: Text('novel-settings.singlePage'.i18n), + ), + // ButtonSegment( + // value: NovelReadMode.doublePage, + // label: Text('novel-settings.doublePage'.i18n), + // ), + ], + selected: {_c.readType.value}, + onSelectionChanged: (value) { + if (value.isNotEmpty) { + _c.readType.value = value.first; + } + }, + showSelectedIcon: false, + ), + ), const SizedBox(height: 16), Text('reader-settings.indicator-alignment'.i18n), const SizedBox(height: 16), @@ -117,6 +145,8 @@ class _NovelReaderSettingsState extends State { value: _c.fontSize.value, onChanged: (value) { _c.fontSize.value = value; + MiruStorage.setSetting( + SettingKey.novelFontSize, value); }, label: _c.fontSize.value.toString(), divisions: 24, @@ -132,11 +162,13 @@ class _NovelReaderSettingsState extends State { value: _c.leading.value, onChanged: (value) { _c.leading.value = value; + MiruStorage.setSetting( + SettingKey.leading, value); }, label: _c.leading.value.toString(), divisions: 40, min: 0, - max: 40, + max: 4, ), ), ], diff --git a/lib/views/widgets/watch/control_panel_header.dart b/lib/views/widgets/watch/control_panel_header.dart index f0cfb113..3558814d 100644 --- a/lib/views/widgets/watch/control_panel_header.dart +++ b/lib/views/widgets/watch/control_panel_header.dart @@ -30,58 +30,82 @@ class _ControlPanelHeaderState Widget _buildAndroid(BuildContext context) { return SafeArea( - child: Container( - height: 60, - color: Theme.of(context).scaffoldBackgroundColor, - child: AppBar( - title: Text(_c.title), - actions: [ - IconButton( + child: Column(children: [ + Container( + height: 60, + color: Theme.of(context).scaffoldBackgroundColor, + child: AppBar( + title: Text(_c.title), + actions: [ + IconButton( + onPressed: () { + _c.enableAutoScroll.value = !_c.enableAutoScroll.value; + }, + icon: _c.enableAutoScroll.value + ? const Icon(Icons.stop_rounded) + : const Icon(Icons.play_arrow_rounded)), + IconButton( onPressed: () { - _c.enableAutoScroll.value = !_c.enableAutoScroll.value; + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => DraggableScrollableSheet( + expand: false, + builder: (context, controller) => SingleChildScrollView( + controller: controller, + child: widget.buildSettings(context)), + ), + ); }, - icon: _c.enableAutoScroll.value - ? const Icon(Icons.stop_rounded) - : const Icon(Icons.play_arrow_rounded)), - IconButton( - onPressed: () { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (context) => DraggableScrollableSheet( - expand: false, - builder: (context, controller) => SingleChildScrollView( - controller: controller, - child: widget.buildSettings(context)), - ), - ); - }, - icon: const Icon(Icons.settings), - ), - IconButton( - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) { - return Obx( - () => PlayList( - title: _c.title, - list: _c.playList.map((e) => e.name).toList(), - selectIndex: _c.index.value, - onChange: (value) { - _c.index.value = value; - Get.back(); - }, - ), - ); - }, - ); - }, - icon: const Icon(Icons.list), - ), - ], + icon: const Icon(Icons.settings), + ), + IconButton( + onPressed: () { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (context) => DraggableScrollableSheet( + expand: false, + builder: (context, controller) { + return Obx( + () => PlayList( + title: _c.title, + list: _c.playList.map((e) => e.name).toList(), + selectIndex: _c.index.value, + scrollController: controller, + onChange: (value) { + _c.clearData(); + _c.index.value = value; + _c.getContent(); + Get.back(); + }, + ), + ); + }), + ); + }, + icon: const Icon(Icons.list), + ), + ], + ), ), - ), + Material( + child: Obx(() => Row(children: [ + const SizedBox(width: 30), + const Icon(Icons.brightness_medium_rounded), + Expanded( + child: Slider( + value: _c.brightness.value, + max: 1, + min: 0, + onChanged: (val) async { + _c.brightness.value = val; + await _c.setBrightness(val); + }, + )), + const SizedBox(width: 30) + ]))) + ]), ).animate().fade(); } @@ -128,7 +152,9 @@ class _ControlPanelHeaderState list: _c.playList.map((e) => e.name).toList(), selectIndex: _c.index.value, onChange: (value) { + _c.clearData(); _c.index.value = value; + _c.getContent(); router.pop(); }, ), diff --git a/lib/views/widgets/watch/playlist.dart b/lib/views/widgets/watch/playlist.dart index 7900921d..0b9fb432 100644 --- a/lib/views/widgets/watch/playlist.dart +++ b/lib/views/widgets/watch/playlist.dart @@ -3,9 +3,10 @@ import 'package:flutter/material.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -class PlayList extends fluent.StatelessWidget { +class PlayList extends StatefulWidget { const PlayList({ super.key, + this.scrollController, required this.title, required this.list, required this.selectIndex, @@ -15,21 +16,42 @@ class PlayList extends fluent.StatelessWidget { final List list; final int selectIndex; final Function(int) onChange; + final ScrollController? scrollController; + @override + State createState() => _PlayListState(); +} + +class _PlayListState extends State { + late final list = widget.list; + late final selectIndex = widget.selectIndex; + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (widget.scrollController == null) { + return; + } + widget.scrollController!.jumpTo(widget.selectIndex * 60); + }); + super.initState(); + } Widget _buildAndroid(BuildContext context) { return Container( - padding: const EdgeInsets.all(8), - color: Theme.of(context).colorScheme.background, - child: ScrollablePositionedList.builder( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).colorScheme.background, + ), + padding: const EdgeInsets.fromLTRB(20, 5, 20, 0), + child: ListView.builder( itemCount: list.length, - initialScrollIndex: selectIndex, + controller: widget.scrollController, itemBuilder: (context, index) { final contact = list[index]; return PlaylistAndroidTile( title: contact, selected: list[selectIndex] == contact, onTap: () { - onChange(index); + widget.onChange(index); }, ); }, @@ -50,7 +72,7 @@ class PlayList extends fluent.StatelessWidget { title: Text(contact), selected: list[selectIndex] == contact, onSelectionChange: (value) { - onChange(index); + widget.onChange(index); }, ); }, diff --git a/lib/views/widgets/watch/reader_view.dart b/lib/views/widgets/watch/reader_view.dart index 49f4421f..1c2d7887 100644 --- a/lib/views/widgets/watch/reader_view.dart +++ b/lib/views/widgets/watch/reader_view.dart @@ -41,7 +41,7 @@ class ReaderView extends StatelessWidget { // 点击中间显示控制面板 // 左边上一页右边下一页 - if (c.error.value.isEmpty) ...[ + if (c.error.value.isEmpty || !c.enableTapRegion.value) ...[ Padding( padding: EdgeInsets.fromLTRB( 0, 120, width - c.prevPageHitBox.value * width, 120), diff --git a/pubspec.lock b/pubspec.lock index a56270c7..aff1f870 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" + bookfx: + dependency: "direct main" + description: + name: bookfx + sha256: "115e64b263d077943440c885acf4a249bcdb1ba5a2ad26c41b9c027292fa0010" + url: "https://pub.dev" + source: hosted + version: "0.0.2" boolean_selector: dependency: transitive description: @@ -1103,7 +1111,7 @@ packages: source: hosted version: "1.0.2" screen_brightness: - dependency: transitive + dependency: "direct main" description: name: screen_brightness sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd diff --git a/pubspec.yaml b/pubspec.yaml index ba7ec4f6..6920e935 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -63,6 +63,8 @@ dependencies: battery_plus: ^5.0.2 based_battery_indicator: ^1.0.3 flutter_tts: ^3.8.5 + screen_brightness: ^0.2.2+1 + bookfx: ^0.0.2 dev_dependencies: flutter_test: sdk: flutter From 13e8edee61e32a10724eecf5f8e8bb775f20106e Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sat, 17 Feb 2024 00:45:52 +0800 Subject: [PATCH 16/24] update desktop reader command bar --- assets/i18n/en.json | 10 +- lib/controllers/watch/comic_controller.dart | 8 + lib/controllers/watch/novel_controller.dart | 11 +- lib/controllers/watch/reader_controller.dart | 7 - .../reader/comic/comic_reader_settings.dart | 253 +++++++------- .../reader/novel/novel_reader_content.dart | 8 +- .../reader/novel/novel_reader_settings.dart | 322 ++++++++++++++++-- .../widgets/watch/control_panel_footer.dart | 4 +- .../widgets/watch/desktop_command_bar.dart | 189 +++++++++- 9 files changed, 631 insertions(+), 181 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 4efac953..ca8b7aec 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -205,7 +205,8 @@ "time": "Time", "page-indicator": "Page Indicator", "battery-icon": "Battery Icon", - "indicator-alignment": "Indicator Alignment" + "indicator-alignment": "Indicator Alignment", + "enable-fullScreen": "Enable Full Screen" }, "novel-settings": { "font-size": "Font size", @@ -216,9 +217,10 @@ "tts-volume": "TTS Volume", "tts-pitch": "TTS Pitch", "text-color": "Text Color", - "heighlight-color": "Highlight Color", - "heighlight-text-color": "Highlight Text Color", - "leading": "Leading" + "highlight-color": "Highlight Background Color", + "highlight-text-color": "Highlight Text Color", + "leading": "Leading", + "color-settings":"Color Settings" }, "bugreport": { "auto-remove-subtitle": "delete in ~ days", diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 65514d88..1bfdb1c0 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -73,6 +73,13 @@ class ComicController extends ReaderController { // ever(height, (callback) { // super.height.value = callback; // }); + mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { + if (setControllPanel.value) { + isShowControlPanel.value = true; + return; + } + isShowControlPanel.value = false; + }); ever(readType, (callback) { _jumpPage(currentGlobalProgress.value); // 保存设置 @@ -106,6 +113,7 @@ class ComicController extends ReaderController { currentGlobalProgress.value = callback; _jumpPage(callback); }); + ever(currentGlobalProgress, (callback) { if (updateSlider.value) { progress.value = callback; diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index c15ca4a3..a2e15841 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -45,8 +45,8 @@ class NovelController extends ReaderController { late final RxList subtitles = List.generate(playList.length, (index) => "").obs; final Rx textColor = Colors.white.obs; - final Rx heighLightColor = Colors.blue.obs; - final Rx heighLightTextColor = Colors.white.obs; + final Rx highLightColor = Colors.blue.obs; + final Rx highLightTextColor = Colors.white.obs; final RxInt currentLine = 0.obs; final RxDouble leading = 20.0.obs; final RxInt bookPage = (-1).obs; @@ -96,6 +96,13 @@ class NovelController extends ReaderController { enableSelectText.value = false; hideControlPanel(); }); + mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { + if (setControllPanel.value) { + isShowControlPanel.value = true; + return; + } + isShowControlPanel.value = false; + }); ever(readType, (callback) { if (callback == NovelReadMode.scroll) { bookPage.value = -1; diff --git a/lib/controllers/watch/reader_controller.dart b/lib/controllers/watch/reader_controller.dart index 8261424e..03253e4f 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -106,13 +106,6 @@ abstract class ReaderController extends GetxController { _barreryTimer = Timer.periodic(const Duration(seconds: 10), (timer) => _statusBar()); - mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { - if (setControllPanel.value) { - isShowControlPanel.value = true; - return; - } - isShowControlPanel.value = false; - }); super.onInit(); } diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index f8061b59..35c4a404 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -1,6 +1,7 @@ import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:flutter/material.dart'; // import 'package:flutter_box_transform/flutter_box_transform.dart'; +import 'package:miru_app/views/widgets/watch/desktop_command_bar.dart'; import 'package:get/get.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/comic_controller.dart'; @@ -22,8 +23,17 @@ class ComicReaderSettings extends StatefulWidget { class _ComicReaderSettingsState extends State { late final ComicController _c = Get.find(tag: widget.tag); final fluent.FlyoutController _readModeFlyout = fluent.FlyoutController(); - // double nextPageHitBox = MiruStorage.getSetting(SettingKey.nextPageHitBox); - // double prevPageHitBox = MiruStorage.getSetting(SettingKey.prevPageHitBox); + final fluent.FlyoutController _indicatorConfigFlyout = + fluent.FlyoutController(); + final fluent.FlyoutController _indicatorAlignmentFlyout = + fluent.FlyoutController(); + final alignMode = { + "comic-settings.bottomLeft".i18n: Alignment.bottomLeft, + "comic-settings.bottomRight".i18n: Alignment.bottomRight, + "comic-settings.topLeft".i18n: Alignment.topLeft, + "comic-settings.topRight".i18n: Alignment.topRight, + }.obs; + Widget _buildAndroid(BuildContext context) { return DefaultTabController( length: 2, @@ -242,7 +252,13 @@ class _ComicReaderSettingsState extends State { } Widget _buildDesktop(BuildContext context) { - return Obx(() => fluent.CommandBar( + return Obx(() => fluent.CommandBarCard( + // backgroundColor: fluent.FluentTheme.of(context).micaBackgroundColor, + borderRadius: const BorderRadius.all(Radius.circular(10)), + margin: const EdgeInsets.fromLTRB(40, 20, 40, 0), + padding: const EdgeInsets.fromLTRB(0, 0, 0, 20), + child: fluent.CommandBar( + isCompact: true, primaryItems: [ CommandBarFlyOutTarget( controller: _readModeFlyout, @@ -258,42 +274,19 @@ class _ComicReaderSettingsState extends State { onPressed: () { _readModeFlyout.showFlyout( builder: (context) => fluent.MenuFlyout( - items: [ - fluent.MenuFlyoutItem( - leading: _c.readType.value == - MangaReadMode.standard - ? const Icon( - fluent.FluentIcons.location_dot) - : null, - text: Text("comic-settings.standard".i18n), - onPressed: () { - _c.readType.value = - MangaReadMode.standard; - }), - fluent.MenuFlyoutItem( - leading: _c.readType.value == - MangaReadMode.rightToLeft - ? const Icon( - fluent.FluentIcons.location_dot) - : null, - text: Text( - "comic-settings.right-to-left".i18n), - onPressed: () { - _c.readType.value = - MangaReadMode.rightToLeft; - }), - fluent.MenuFlyoutItem( - leading: _c.readType.value == - MangaReadMode.webTonn - ? const Icon( - fluent.FluentIcons.location_dot) - : null, - text: Text("comic-settings.web-tonn".i18n), - onPressed: () { - _c.readType.value = MangaReadMode.webTonn; - }) - ], - )); + items: _c.readmode.keys + .map((e) => fluent.MenuFlyoutItem( + leading: _c.readType.value == + _c.readmode[e]! + ? const Icon( + fluent.FluentIcons.location_dot) + : null, + onPressed: () { + _c.readType.value = _c.readmode[e]!; + }, + text: Text(e), + )) + .toList())); }, )), fluent.CommandBarBuilderItem( @@ -320,7 +313,7 @@ class _ComicReaderSettingsState extends State { child: w, )), CommandBarText(text: "/ ${_c.watchData.value?.urls.length ?? 0}"), - const fluent.CommandBarSeparator(thickness: 3), + const CommnadBarDivider(), fluent.CommandBarBuilderItem( builder: (context, mode, w) => Tooltip( message: "reader-settings.enable-wakelock".i18n, @@ -336,7 +329,7 @@ class _ComicReaderSettingsState extends State { child: const Icon(fluent.FluentIcons.coffee_script, size: 17)), ), - const fluent.CommandBarSeparator(thickness: 3), + const CommnadBarDivider(), fluent.CommandBarBuilderItem( builder: (context, mode, w) => Tooltip( message: "reader-settings.enable-fullScreen".i18n, @@ -349,9 +342,109 @@ class _ComicReaderSettingsState extends State { }, checked: _c.enableFullScreen.value, child: const Icon(fluent.FluentIcons.full_screen, size: 17)), - ) + ), + const CommnadBarDivider(), + CommandBarFlyOutTarget( + controller: _indicatorConfigFlyout, + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: fluent.IconButton( + icon: Row(children: [ + const Icon(fluent.FluentIcons.number_field, size: 17), + const SizedBox(width: 8), + Text("comic-settings.status-bar".i18n) + ]), + onPressed: () { + _indicatorConfigFlyout.showFlyout( + builder: (context) => fluent.FlyoutContent( + constraints: + const BoxConstraints(maxWidth: 200), + child: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + children: List.generate( + _c.statusBarElement.length, + (index) => fluent.FlyoutListTile( + onPressed: () { + _c + .statusBarElement[ + _c.statusBarElement + .keys + .elementAt( + index)]! + .value = + !_c + .statusBarElement[_c + .statusBarElement + .keys + .elementAt( + index)]! + .value; + }, + text: Row(children: [ + fluent.Checkbox( + checked: _c + .statusBarElement.values + .elementAt(index) + .value, + onChanged: (val) { + if (val == null) { + return; + } + _c + .statusBarElement[_c + .statusBarElement + .keys + .elementAt( + index)]! + .value = val; + }, + ), + const SizedBox(width: 8), + Text(_c.statusBarElement.keys + .elementAt(index)) + ]), + )))), + )); + }, + ))), + const CommnadBarDivider(), + CommandBarFlyOutTarget( + controller: _indicatorAlignmentFlyout, + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: fluent.IconButton( + icon: Row(children: [ + const Icon(fluent.FluentIcons.align_center, size: 17), + const SizedBox(width: 8), + Text("comic-settings.indicator-alignment".i18n) + ]), + onPressed: () { + _indicatorAlignmentFlyout.showFlyout( + builder: (context) => fluent.FlyoutContent( + constraints: + const BoxConstraints(maxWidth: 200), + child: Obx(() => Column( + mainAxisSize: MainAxisSize.min, + children: List.generate( + alignMode.keys.length, + (index) => fluent.FlyoutListTile( + onPressed: () { + _c.alignMode.value = alignMode + .values + .elementAt(index); + }, + selected: _c.alignMode.value == + alignMode.values + .elementAt(index), + text: Text(alignMode.keys + .elementAt(index)), + )))), + )); + }, + )), + ), ], - )); + ))); } @override @@ -362,77 +455,3 @@ class _ComicReaderSettingsState extends State { ); } } - -class CommandBarDropDownButton extends fluent.CommandBarItem { - const CommandBarDropDownButton( - {super.key, required this.items, this.onPressed, this.icon, this.label}); - final List items; - final VoidCallback? onPressed; - final Widget? icon; - final Widget? label; - - @override - Widget build( - BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { - return Row(mainAxisSize: MainAxisSize.min, children: [ - if (icon != null) ...[icon!, const SizedBox(width: 8)], - fluent.DropDownButton(items: items) - ]); - } -} - -class CommandBarFlyOutTarget extends fluent.CommandBarItem { - const CommandBarFlyOutTarget( - {super.key, required this.controller, required this.child, this.label}); - final fluent.FlyoutController controller; - final Widget child; - final Widget? label; - @override - Widget build( - BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { - return Row(mainAxisSize: MainAxisSize.min, children: [ - if (label != null) ...[ - label!, - const SizedBox( - width: 6.0, - ) - ], - fluent.FlyoutTarget( - controller: controller, - child: child, - ) - ]); - } -} - -class CommandBarText extends fluent.CommandBarItem { - const CommandBarText({super.key, required this.text}); - final String text; - @override - Widget build( - BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { - return Padding(padding: const EdgeInsets.all(10), child: Text(text)); - } -} - -class CommandBarToggleButton extends fluent.CommandBarItem { - const CommandBarToggleButton( - {super.key, - required this.onchange, - required this.checked, - required this.child}); - final bool checked; - final void Function(bool)? onchange; - final Widget child; - @override - Widget build( - BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { - return Padding( - padding: const EdgeInsets.all(10), - child: fluent.ToggleButton( - checked: checked, - onChanged: onchange, - child: child, - )); - } -} diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index aa7cfd03..ef17d0fb 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -313,12 +313,12 @@ class _NovelReaderContentState extends State { text: content[index], style: TextStyle( color: index == _c.currentLine.value - ? _c.heighLightTextColor.value + ? _c.highLightTextColor.value : _c.textColor.value, fontSize: fontSize, fontWeight: FontWeight.w400, backgroundColor: - index == _c.currentLine.value ? _c.heighLightColor.value : null, + index == _c.currentLine.value ? _c.highLightColor.value : null, height: 2, textBaseline: TextBaseline.ideographic, fontFamily: 'Microsoft Yahei', @@ -343,12 +343,12 @@ class _NovelReaderContentState extends State { text: content[index], style: TextStyle( color: index == _c.currentLine.value - ? _c.heighLightTextColor.value + ? _c.highLightTextColor.value : _c.textColor.value, fontSize: fontSize, fontWeight: FontWeight.w400, backgroundColor: index == _c.currentLine.value - ? _c.heighLightColor.value + ? _c.highLightColor.value : null, height: leading, textBaseline: TextBaseline.ideographic, diff --git a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart index 65195404..02e12aea 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart @@ -8,6 +8,8 @@ import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/utils/miru_storage.dart'; import 'package:miru_app/views/widgets/settings/settings_switch_tile.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:miru_app/views/widgets/watch/desktop_command_bar.dart'; +import 'package:window_manager/window_manager.dart'; class NovelReaderSettings extends StatefulWidget { const NovelReaderSettings(this.tag, {super.key}); @@ -19,7 +21,17 @@ class NovelReaderSettings extends StatefulWidget { class _NovelReaderSettingsState extends State { late final NovelController _c = Get.find(tag: widget.tag); - + // final fluent.FlyoutController _readModeFlyout = fluent.FlyoutController(); + // final fluent.FlyoutController _indicatorConfigFlyout = + // fluent.FlyoutController(); + // final fluent.FlyoutController _indicatorAlignmentFlyout = + // fluent.FlyoutController(); + final alignMode = { + "comic-settings.bottomLeft".i18n: Alignment.bottomLeft, + "comic-settings.bottomRight".i18n: Alignment.bottomRight, + "comic-settings.topLeft".i18n: Alignment.topLeft, + "comic-settings.topRight".i18n: Alignment.topRight, + }.obs; Widget _buildAndroid(BuildContext context) { return DefaultTabController( length: 3, @@ -313,7 +325,7 @@ class _NovelReaderSettingsState extends State { (index) => ChoiceChip( onSelected: (val) { if (val) { - _c.heighLightColor.value = + _c.highLightColor.value = ColorUtils.baseColors[index]; } }, @@ -325,7 +337,7 @@ class _NovelReaderSettingsState extends State { ), ), selected: ColorUtils.baseColors[index] == - _c.heighLightColor.value)), + _c.highLightColor.value)), ), const SizedBox(height: 16), Text("novel-settings.heighlight-text-color".i18n), @@ -337,7 +349,7 @@ class _NovelReaderSettingsState extends State { (index) => ChoiceChip( onSelected: (val) { if (val) { - _c.heighLightTextColor.value = + _c.highLightTextColor.value = ColorUtils.baseColors[index]; } }, @@ -349,7 +361,7 @@ class _NovelReaderSettingsState extends State { ), ), selected: ColorUtils.baseColors[index] == - _c.heighLightTextColor.value)), + _c.highLightTextColor.value)), ), ], )), @@ -361,30 +373,286 @@ class _NovelReaderSettingsState extends State { } Widget _buildDesktop(BuildContext context) { - return fluent.Card( - backgroundColor: fluent.FluentTheme.of(context).micaBackgroundColor, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text("novel-settings.font-size".i18n), - const SizedBox(height: 16), - Obx( - () => SizedBox( - width: 200, - child: fluent.Slider( - value: _c.fontSize.value, - onChanged: (value) { - _c.fontSize.value = value; - }, - min: 12, - max: 24, + return Obx(() => fluent.CommandBarCard( + // backgroundColor: fluent.FluentTheme.of(context).micaBackgroundColor, + borderRadius: const BorderRadius.all(Radius.circular(10)), + margin: const EdgeInsets.fromLTRB(40, 20, 40, 0), + padding: const EdgeInsets.fromLTRB(0, 0, 0, 20), + child: fluent.CommandBar( + isCompact: true, + primaryItems: [ + const CommandBarSpacer(width: 16), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: "comic-settings.read-mode".i18n, child: w), + wrappedItem: CommandBarDropDownButton( + leading: const Icon( + fluent.FluentIcons.reading_mode, + size: 17, + ), + items: _c.readmode.keys + .map((e) => fluent.MenuFlyoutItem( + text: Text(e), + leading: _c.readType.value == _c.readmode[e]! + ? const Icon(fluent.FluentIcons.location_dot) + : null, + onPressed: () { + _c.readType.value = _c.readmode[e]!; + })) + .toList())), + const CommandBarSpacer(), + const CommnadBarDivider(), + fluent.CommandBarBuilderItem( + wrappedItem: fluent.CommandBarButton( + label: SizedBox( + width: 40, + child: fluent.NumberBox( + max: _c.watchData.value?.content.length ?? 1, + min: 1, + mode: fluent.SpinButtonPlacementMode.none, + clearButton: false, + value: _c.progress.value + 1, + onChanged: (value) { + if (value != null) { + _c.updateSlider.value = true; + _c.progress.value = value - 1; + } + }, + )), + onPressed: null, + ), + builder: (context, mode, w) => fluent.Tooltip( + message: "comic-settings.page".i18n, + child: w, + )), + CommandBarText( + text: "/ ${_c.watchData.value?.content.length ?? 0}"), + const CommnadBarDivider(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: "reader-settings.enable-wakelock".i18n, + child: w, ), + wrappedItem: CommandBarToggleButton( + onchange: (val) { + _c.enableWakeLock.value = val; + WakelockPlus.toggle(enable: val); + MiruStorage.setSetting(SettingKey.enableWakelock, val); + }, + checked: _c.enableWakeLock.value, + child: + const Icon(fluent.FluentIcons.coffee_script, size: 17)), ), - ), - ], - ), - ); + const CommnadBarDivider(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: "reader-settings.enable-fullScreen".i18n, + child: w, + ), + wrappedItem: CommandBarToggleButton( + onchange: (val) async { + _c.enableFullScreen.value = val; + await windowManager.setFullScreen(val); + }, + checked: _c.enableFullScreen.value, + child: const Icon(fluent.FluentIcons.full_screen, size: 17)), + ), + const CommnadBarDivider(), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: ("comic-settings.status-bar".i18n), + child: w, + ), + wrappedItem: CommandBarDropDownButton( + leading: + const Icon(fluent.FluentIcons.number_field, size: 17), + items: _c.statusBarElement.keys + .map((e) => fluent.MenuFlyoutItem( + leading: fluent.Checkbox( + checked: _c.statusBarElement[e]!.value, + onChanged: (val) { + if (val == null) { + return; + } + _c.statusBarElement[e]!.value = val; + }, + ), + text: Text(e), + onPressed: () { + _c.statusBarElement[e]!.value = + !_c.statusBarElement[e]!.value; + })) + .toList(), + )), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (conetx, mode, w) => fluent.Tooltip( + message: "comic-settings.indicator-alignment".i18n, + child: w, + ), + wrappedItem: CommandBarDropDownButton( + items: alignMode.keys + .map((e) => fluent.MenuFlyoutItem( + leading: _c.alignMode.value == alignMode[e]! + ? const Icon(fluent.FluentIcons.location_dot) + : null, + text: Text(e), + onPressed: () { + _c.alignMode.value = alignMode[e]!; + })) + .toList(), + leading: + const Icon(fluent.FluentIcons.align_center, size: 17))), + const CommandBarSpacer(), + const CommnadBarDivider(), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (context, displayMode, w) => fluent.Tooltip( + message: "novel-settings.highlight-text-color".i18n, + child: w, + ), + wrappedItem: CommandBarDropDownButton( + title: Stack(children: [ + const Icon( + fluent.FluentIcons.fabric_text_highlight, + size: 17, + // color: _c.heighLightTextColor.value, + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Container( + width: 17, + height: 3, + color: _c.highLightTextColor.value, + )) + ]), + items: ColorUtils.baseColors + .map((e) => fluent.MenuFlyoutItem( + text: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e, + ), + child: e == Colors.transparent + ? const Icon( + fluent.FluentIcons.clear, + size: 17, + ) + : null, + ), + onPressed: () { + _c.highLightTextColor.value = e; + })) + .toList())), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (context, displayMode, w) => fluent.Tooltip( + message: "novel-settings.text-color".i18n, + child: w, + ), + wrappedItem: CommandBarDropDownButton( + title: Stack(children: [ + const Icon( + fluent.FluentIcons.font_color_a, + size: 17, + // color: _c.textColor.value, + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Container( + width: 17, + height: 3, + color: _c.textColor.value, + )) + ]), + items: ColorUtils.baseColors + .map((e) => fluent.MenuFlyoutItem( + text: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e, + ), + child: e == Colors.transparent + ? const Icon( + fluent.FluentIcons.clear, + size: 17, + ) + : null, + ), + onPressed: () { + _c.textColor.value = e; + })) + .toList())), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: "novel-settings.highlight-color".i18n, + child: w, + ), + wrappedItem: CommandBarDropDownButton( + title: Stack(children: [ + const Icon( + fluent.FluentIcons.highlight, + size: 17, + // color: _c.highLightColor.value, + ), + Padding( + padding: const EdgeInsets.only(top: 20), + child: Container( + width: 17, + height: 3, + color: _c.highLightColor.value, + )) + ]), + items: ColorUtils.baseColors + .map((e) => fluent.MenuFlyoutItem( + text: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e, + ), + child: e == Colors.transparent + ? const Icon( + fluent.FluentIcons.clear, + size: 17, + ) + : null, + ), + onPressed: () { + _c.highLightColor.value = e; + })) + .toList())), + const CommandBarSpacer(), + const CommnadBarDivider(), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (contex, mode, w) => fluent.Tooltip( + message: "novel-settings.font-size".i18n, + child: w, + ), + wrappedItem: CommandBarNumberBox( + onchange: (value) { + if (value != null) { + _c.fontSize.value = value; + MiruStorage.setSetting(SettingKey.novelFontSize, value); + } + }, + value: _c.fontSize.value, + min: 1, + max: 30, + title: const Icon( + fluent.FluentIcons.font_size, + size: 17, + ))) + ], + ))); } @override diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index f0bbfb45..8ee1064a 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -219,7 +219,7 @@ class _ControlPanelFooterState const Spacer(flex: 10), _desktopMangaPlayerButton( 20, fluent.FluentIcons.previous, () { - _c.index.value--; + _c.prevChap(); }), const Spacer(), _desktopMangaPlayerButton( @@ -233,7 +233,7 @@ class _ControlPanelFooterState const Spacer(), _desktopMangaPlayerButton( 20, fluent.FluentIcons.next, () { - _c.index.value++; + _c.nextChap(); }), const Spacer(flex: 10), fluent.FlyoutTarget( diff --git a/lib/views/widgets/watch/desktop_command_bar.dart b/lib/views/widgets/watch/desktop_command_bar.dart index e28c9e36..13c83927 100644 --- a/lib/views/widgets/watch/desktop_command_bar.dart +++ b/lib/views/widgets/watch/desktop_command_bar.dart @@ -1,18 +1,171 @@ -// import 'package:flutter/material.dart'; - -// class DesktopCommandBar extends StatelessWidget { -// const DesktopCommandBar( -// {super.key, required this.text, this.icon, required this.onPressed}); -// final Widget text; -// final Widget? icon; -// final VoidCallback onPressed; - -// @override -// Widget build(BuildContext context) { -// return Padding( -// padding: const EdgeInsets.all(4), -// child: Row( -// children: [], -// )); -// } -// } +import 'package:flutter/material.dart'; +import 'package:fluent_ui/fluent_ui.dart' as fluent; + +class CommandBarDropDownButton extends fluent.CommandBarItem { + const CommandBarDropDownButton( + {super.key, + required this.items, + this.onPressed, + this.icon, + this.leading, + this.title}); + final List items; + final VoidCallback? onPressed; + final Widget? icon; + final Widget? leading; + final Widget? title; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return fluent.DropDownButton( + leading: leading, + title: title, + items: items, + closeAfterClick: false, + ); + } +} + +class CommandBarFlyOutTarget extends fluent.CommandBarItem { + const CommandBarFlyOutTarget( + {super.key, required this.controller, required this.child, this.label}); + final fluent.FlyoutController controller; + final Widget child; + final Widget? label; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Row(mainAxisSize: MainAxisSize.min, children: [ + if (label != null) ...[ + label!, + const SizedBox( + width: 6.0, + ) + ], + fluent.FlyoutTarget( + controller: controller, + child: child, + ) + ]); + } +} + +class CommandBarText extends fluent.CommandBarItem { + const CommandBarText({super.key, required this.text}); + final String text; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Padding(padding: const EdgeInsets.all(10), child: Text(text)); + } +} + +class CommandBarToggleButton extends fluent.CommandBarItem { + const CommandBarToggleButton( + {super.key, + required this.onchange, + required this.checked, + required this.child}); + final bool checked; + final void Function(bool)? onchange; + final Widget child; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Padding( + padding: const EdgeInsets.all(10), + child: fluent.ToggleButton( + checked: checked, + onChanged: onchange, + child: child, + )); + } +} + +class CommnadBarDivider extends fluent.CommandBarItem { + const CommnadBarDivider({super.key, this.thickness, this.height, this.color}); + final double? thickness; + final double? height; + final Color? color; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Container( + height: height ?? 20.0, + width: thickness ?? 3.0, + decoration: BoxDecoration( + color: fluent.FluentTheme.of(context).brightness == + fluent.Brightness.dark + ? const Color(0xFF484848) + : const Color(0xFFB7B7B7)), + ); + } +} + +class CommandBarSpacer extends fluent.CommandBarItem { + const CommandBarSpacer({super.key, this.width}); + final double? width; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return SizedBox(width: width ?? 8.0); + } +} + +class CommandBarSplitButton extends fluent.CommandBarItem { + const CommandBarSplitButton( + {super.key, + required this.child, + required this.flyOutWidget, + this.onInvoked, + this.label, + this.flyoutController}); + final Widget child; + final VoidCallback? onInvoked; + final Widget flyOutWidget; + final Widget? label; + final GlobalKey? flyoutController; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Row(mainAxisSize: MainAxisSize.min, children: [ + fluent.SplitButton( + flyout: flyOutWidget, + onInvoked: onInvoked, + child: child, + ) + ]); + } +} + +class CommandBarNumberBox extends fluent.CommandBarItem { + const CommandBarNumberBox( + {super.key, + required this.onchange, + required this.value, + required this.min, + required this.max, + this.title}); + final void Function(double?)? onchange; + final double value; + final double min; + final double max; + final Widget? title; + @override + Widget build( + BuildContext context, fluent.CommandBarItemDisplayMode displayMode) { + return Row(mainAxisSize: MainAxisSize.min, children: [ + if (title != null) ...[title!, const SizedBox(width: 8.0)], + SizedBox( + width: 50, + child: fluent.NumberBox( + value: value, + min: min, + max: max, + onChanged: onchange, + mode: fluent.SpinButtonPlacementMode.none, + clearButton: false, + )) + ]); + } +} From f7b024ae88ade48ebdc10100179d6c92e6ffe05f Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:15:48 +0800 Subject: [PATCH 17/24] fix:i18n --- assets/i18n/en.json | 7 +- lib/utils/miru_storage.dart | 112 +++++++----------- .../reader/comic/comic_reader_settings.dart | 44 +++---- 3 files changed, 64 insertions(+), 99 deletions(-) diff --git a/assets/i18n/en.json b/assets/i18n/en.json index 9185eac7..948100bc 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -233,7 +233,12 @@ "topLeft": "Top left", "topRight": "Top right", "indicator-alignment": "Indicator alignment", - "status-bar":"Status bar" + "status-bar":"Status bar", + "prevPageHitBox": "Tapping region of previous page", + "nextPageHitBox": "Tapping region of next page", + "enable-autoscroller": "Enable autoscroller", + "autoscroller-interval": "Autoscroller interval", + "autoscroller-offset": "Autoscroller offset" }, "reader-settings": { "enable-wakelock": "Keep screen on", diff --git a/lib/utils/miru_storage.dart b/lib/utils/miru_storage.dart index b81224b0..48b92200 100644 --- a/lib/utils/miru_storage.dart +++ b/lib/utils/miru_storage.dart @@ -179,73 +179,49 @@ class MiruStorage { } class SettingKey { - static String theme = "Theme"; - static String miruRepoUrl = "MiruRepoUrl"; - static String tmdbKey = 'TMDBKey'; - static String autoCheckUpdate = 'AutoCheckUpdate'; - static String language = 'Language'; - static String novelFontSize = 'NovelFontSize'; - static String enableNSFW = 'EnableNSFW'; - static String videoPlayer = 'VideoPlayer'; - static String databaseVersion = 'DatabaseVersion'; - static String listMode = 'ListMode'; - static String keyI = 'KeyI'; - static String keyJ = 'KeyJ'; - static String arrowLeft = 'Arrowleft'; - static String arrowRight = 'Arrowright'; + static const String theme = "Theme"; + static const String miruRepoUrl = "MiruRepoUrl"; + static const String tmdbKey = 'TMDBKey'; + static const String autoCheckUpdate = 'AutoCheckUpdate'; + static const String language = 'Language'; + static const String novelFontSize = 'NovelFontSize'; + static const String enableNSFW = 'EnableNSFW'; + static const String videoPlayer = 'VideoPlayer'; + static const String databaseVersion = 'DatabaseVersion'; + static const String listMode = 'ListMode'; + static const String keyI = 'KeyI'; + static const String keyJ = 'KeyJ'; + static const String arrowLeft = 'Arrowleft'; + static const String arrowRight = 'Arrowright'; //reading mode - static String readingMode = 'ReadingMode'; - static String enableWakelock = 'EnableWakelock'; - static String aniListToken = 'AniListToken'; - static String aniListUserId = 'AniListUserId'; - static String autoTracking = 'AutoTracking'; - static String windowSize = 'WindowsSize'; - static String windowPosition = 'WindowsPosition'; - static String androidWebviewUA = "AndroidWebviewUA"; - static String windowsWebviewUA = "WindowsWebviewUA"; - static String proxy = "Proxy"; - static String proxyType = "ProxyType"; - static String saveLog = "SaveLog"; - static String nextPageHitBox = "NextPageHitBox"; - static String prevPageHitBox = "PrevPageHitBox"; - static String autoScrollInterval = "AutoScrollInterval"; - static String autoScrollOffset = "AutoScrollOffset"; - static String ttsLanguage = "TTSLanguage"; - static String ttsPitch = "TTSPitch"; - static String ttsRate = "TTSRate"; - static String ttsVolume = "TTSVolume"; - static String leading = "Leading"; - static const theme = "Theme"; - static const miruRepoUrl = "MiruRepoUrl"; - static const tmdbKey = 'TMDBKey'; - static const autoCheckUpdate = 'AutoCheckUpdate'; - static const language = 'Language'; - static const novelFontSize = 'NovelFontSize'; - static const enableNSFW = 'EnableNSFW'; - static const videoPlayer = 'VideoPlayer'; - static const databaseVersion = 'DatabaseVersion'; - static const listMode = 'ListMode'; - static const keyI = 'KeyI'; - static const keyJ = 'KeyJ'; - static const arrowLeft = 'Arrowleft'; - static const arrowRight = 'Arrowright'; - static const readingMode = 'ReadingMode'; - static const aniListToken = 'AniListToken'; - static const aniListUserId = 'AniListUserId'; - static const autoTracking = 'AutoTracking'; - static const windowSize = 'WindowsSize'; - static const windowPosition = 'WindowsPosition'; - static const androidWebviewUA = "AndroidWebviewUA"; - static const windowsWebviewUA = "WindowsWebviewUA"; - static const proxy = "Proxy"; - static const proxyType = "ProxyType"; - static const saveLog = "SaveLog"; - static const subtitleFontSize = "SubtitleFontSize"; - static const subtitleFontWeight = "SubtitleFontWeight"; - static const subtitleFontColor = "SubtitleFontColor"; - static const subtitleBackgroundColor = "SubtitleBackgroundColor"; - static const subtitleBackgroundOpacity = "SubtitleBackgroundOpacity"; - static const subtitleTextAlign = "SubtitleTextAlign"; - static const subtitleLastLanguageSelected = "SubtitleLastLanguageSelected"; - static const subtitleLastTitleSelected = "SubtitleLastTitleSelected"; + static const String readingMode = 'ReadingMode'; + static const String enableWakelock = 'EnableWakelock'; + static const String aniListToken = 'AniListToken'; + static const String aniListUserId = 'AniListUserId'; + static const String autoTracking = 'AutoTracking'; + static const String windowSize = 'WindowsSize'; + static const String windowPosition = 'WindowsPosition'; + static const String androidWebviewUA = "AndroidWebviewUA"; + static const String windowsWebviewUA = "WindowsWebviewUA"; + static const String proxy = "Proxy"; + static const String proxyType = "ProxyType"; + static const String saveLog = "SaveLog"; + static const String nextPageHitBox = "NextPageHitBox"; + static const String prevPageHitBox = "PrevPageHitBox"; + static const String autoScrollInterval = "AutoScrollInterval"; + static const String autoScrollOffset = "AutoScrollOffset"; + static const String ttsLanguage = "TTSLanguage"; + static const String ttsPitch = "TTSPitch"; + static const String ttsRate = "TTSRate"; + static const String ttsVolume = "TTSVolume"; + static const String leading = "Leading"; + static const String subtitleFontSize = "SubtitleFontSize"; + static const String subtitleFontWeight = "SubtitleFontWeight"; + static const String subtitleFontColor = "SubtitleFontColor"; + static const String subtitleBackgroundColor = "SubtitleBackgroundColor"; + static const String subtitleBackgroundOpacity = "SubtitleBackgroundOpacity"; + static const String subtitleTextAlign = "SubtitleTextAlign"; + static const String subtitleLastLanguageSelected = + "SubtitleLastLanguageSelected"; + static const String subtitleLastTitleSelected = "SubtitleLastTitleSelected"; } diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index 35c4a404..64b37e05 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -103,41 +103,25 @@ class _ComicReaderSettingsState extends State { segments: [ ButtonSegment( value: Alignment.bottomLeft, - label: Column(children: [ - Text('comic-settings.bottomLeft'.i18n), - const SizedBox(height: 5), - Transform.rotate( - angle: -3.14, - child: const Icon(Icons.arrow_outward)) - ]), + label: Transform.rotate( + angle: -3.14, + child: const Icon(Icons.arrow_outward)), ), ButtonSegment( value: Alignment.bottomRight, - label: Column(children: [ - Text('comic-settings.bottomRight'.i18n), - const SizedBox(height: 5), - Transform.rotate( - angle: 1.57, - child: const Icon(Icons.arrow_outward)) - ]), + label: Transform.rotate( + angle: 1.57, + child: const Icon(Icons.arrow_outward)), ), ButtonSegment( value: Alignment.topLeft, - label: Column(children: [ - Text('comic-settings.topLeft'.i18n), - const SizedBox(height: 5), - Transform.rotate( - angle: -1.57, - child: const Icon(Icons.arrow_outward)) - ]), + label: Transform.rotate( + angle: -1.57, + child: const Icon(Icons.arrow_outward)), ), - ButtonSegment( + const ButtonSegment( value: Alignment.topRight, - label: Column(children: [ - Text('comic-settings.topRight'.i18n), - const SizedBox(height: 5), - const Icon(Icons.arrow_outward) - ]), + label: Icon(Icons.arrow_outward), ) ], selected: {_c.alignMode.value}, @@ -212,14 +196,14 @@ class _ComicReaderSettingsState extends State { children: [ SettingsSwitchTile( icon: const Icon(Icons.play_arrow_rounded), - title: "reader-settings.enable-autoscroller".i18n, + title: "comic-settings.enable-autoscroller".i18n, buildValue: () => _c.enableAutoScroll.value, onChanged: (val) { Get.back(); _c.enableAutoScroll.value = val; }), const SizedBox(height: 16), - Text('reader-settings.auto-scroller-interval'.i18n), + Text('comic-settings.autoscroller-interval'.i18n), Slider( value: _c.autoScrollInterval.value.toDouble(), max: 500.0, @@ -231,7 +215,7 @@ class _ComicReaderSettingsState extends State { SettingKey.autoScrollInterval, val.toInt()); }), const SizedBox(height: 16), - Text('reader-settings.auto-scroller-offset'.i18n), + Text('comic-settings.autoscroller-offset'.i18n), Slider( value: _c.autoScrollOffset.value, max: 300.0, From cd7f3d2b6f2f56a6baf064b62663edd5e6d402d3 Mon Sep 17 00:00:00 2001 From: MiaoMint Date: Wed, 21 Feb 2024 05:46:44 +0800 Subject: [PATCH 18/24] Refactor saveImage method to remove unnecessary "mounted" parameter --- .../reader/comic/comic_reader_content.dart | 183 ++++++++---------- lib/views/widgets/cache_network_image.dart | 49 +---- 2 files changed, 86 insertions(+), 146 deletions(-) diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 98bfa759..6b4524d1 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -5,6 +5,7 @@ import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/comic_controller.dart'; import 'package:miru_app/utils/i18n.dart'; +import 'package:miru_app/utils/log.dart'; import 'package:miru_app/views/widgets/button.dart'; import 'package:miru_app/views/widgets/cache_network_image.dart'; @@ -27,17 +28,9 @@ class _ComicReaderContentState extends State { // 按下数量 final List _pointer = []; - // late final List _chapterItems = List.generate( - // _c.playList.length, - // ((val) => Container( - // color: Colors.green, - // width: MediaQuery.of(context).size.width, - // height: MediaQuery.of(context).size.height, - // child: Center(child: Text(val.toString()))))); - // late final List _chapterItems = List.generate( - // _c.playList.length, ((val) => webtoonContent(context, val))); final menuController = fluent.FlyoutController(); final contextAttachKey = GlobalKey(); + _buildPlaceholder(BuildContext context) { final width = MediaQuery.of(context).size.width; final height = MediaQuery.of(context).size.height; @@ -52,13 +45,6 @@ class _ComicReaderContentState extends State { ); } - @override - void initState() { - super.initState(); - // ever(_c.index, - // (callback) => _chapterItems[callback] = webtoonContent(context)); - } - Widget _buildDisplay(Widget child) { if (_c.statusBarElement.values.every((element) => element.value == false)) { return child; @@ -66,20 +52,25 @@ class _ComicReaderContentState extends State { return Stack( children: [ child, - Obx(() => Align( - alignment: _c.alignMode.value, - child: Container( - color: Colors.black.withAlpha(200), - padding: const EdgeInsets.fromLTRB(20, 2, 12, 2), - child: _indicatorBuilder(), - ), - )), + Obx( + () => Align( + alignment: _c.alignMode.value, + child: Container( + color: Colors.black.withAlpha(200), + padding: const EdgeInsets.fromLTRB(20, 2, 12, 2), + child: _indicatorBuilder(), + ), + ), + ), ], ); } Widget _indicatorBuilder() { - return Obx(() => Row(mainAxisSize: MainAxisSize.min, children: [ + return Obx( + () => Row( + mainAxisSize: MainAxisSize.min, + children: [ if (_c.statusBarElement["reader-settings.page-indicator".i18n]! .value) ...[ Text( @@ -116,7 +107,9 @@ class _ComicReaderContentState extends State { ), const SizedBox(width: 8) ], - ])); + ], + ), + ); } Widget webtoonContent(BuildContext context) { @@ -124,55 +117,56 @@ class _ComicReaderContentState extends State { final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; final width = MediaQuery.of(context).size.width; final height = MediaQuery.of(context).size.height; - return Obx(() => SizedBox( - width: width, - height: height, - child: Listener( - onPointerDown: (event) { - _pointer.add(event.pointer); - if (_pointer.length == 2) { - _c.isZoom.value = true; - } - }, - onPointerUp: (event) { - _pointer.remove(event.pointer); - if (_pointer.length == 1) { - _c.isZoom.value = false; - } - }, - child: InteractiveViewer( - scaleEnabled: _c.isZoom.value, - child: ScrollablePositionedList.builder( - physics: _c.isZoom.value - ? const NeverScrollableScrollPhysics() - : null, - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - initialScrollIndex: _c.currentGlobalProgress.value, - itemScrollController: _c.itemScrollController, - itemPositionsListener: _c.itemPositionsListener, - scrollOffsetController: _c.scrollOffsetController, - scrollOffsetListener: _c.scrollOffsetListener, - itemBuilder: (context, index) { - final img = _c.items.expand((element) => element).toList(); - final url = img[index]; - SizedBox( - width: width, - height: height, - child: const Center( - child: Center( - child: ProgressRing(), - ), - ), - ); - return imageBuilder(url); - }, - itemCount: _c.items.expand((element) => element).length, + return Obx( + () => SizedBox( + width: width, + height: height, + child: Listener( + onPointerDown: (event) { + _pointer.add(event.pointer); + if (_pointer.length == 2) { + _c.isZoom.value = true; + } + }, + onPointerUp: (event) { + _pointer.remove(event.pointer); + if (_pointer.length == 1) { + _c.isZoom.value = false; + } + }, + child: InteractiveViewer( + scaleEnabled: _c.isZoom.value, + child: ScrollablePositionedList.builder( + physics: + _c.isZoom.value ? const NeverScrollableScrollPhysics() : null, + padding: EdgeInsets.symmetric( + horizontal: viewPadding, ), + initialScrollIndex: _c.currentGlobalProgress.value, + itemScrollController: _c.itemScrollController, + itemPositionsListener: _c.itemPositionsListener, + scrollOffsetController: _c.scrollOffsetController, + scrollOffsetListener: _c.scrollOffsetListener, + itemBuilder: (context, index) { + final img = _c.items.expand((element) => element).toList(); + final url = img[index]; + SizedBox( + width: width, + height: height, + child: const Center( + child: Center( + child: ProgressRing(), + ), + ), + ); + return imageBuilder(url); + }, + itemCount: _c.items.expand((element) => element).length, ), ), - )); + ), + ), + ); } _buildContent() { @@ -231,23 +225,23 @@ class _ComicReaderContentState extends State { _c.loadNextChapter(); } } - return true; }, ); } //common mode and left to right mode - return Obx(() => NotificationListener( + return Obx( + () => NotificationListener( onNotification: (notification) { final metrics = notification.metrics; if (metrics.atEdge) { bool isTop = metrics.pixels <= 0; if (isTop) { - debugPrint('At the start'); + logger.info('At the start'); _c.loadPrevChapter(); } else { - debugPrint('At the end'); + logger.info('At the end'); _c.loadNextChapter(); } } @@ -263,24 +257,6 @@ class _ComicReaderContentState extends State { scrollDirection: Axis.horizontal, controller: _c.pageController.value, itemBuilder: (BuildContext context, int index) { - // final urls = _c.items[_c.index.value]; - // final url = images[index]; - // if (index == 0) { - // return Container( - // padding: EdgeInsets.symmetric( - // horizontal: viewPadding, - // ), - // color: Colors.red, - // ); - // } - // if (index == _c.itemlength[_c.index.value] - 1) { - // return Container( - // padding: EdgeInsets.symmetric( - // horizontal: viewPadding, - // ), - // color: Colors.red, - // ); - // } final img = _c.items.expand((element) => element).toList(); final url = img[index]; @@ -291,7 +267,9 @@ class _ComicReaderContentState extends State { child: imageBuilder(url), ); }, - ))); + ), + ), + ); }); }), ), @@ -322,7 +300,10 @@ class _ComicReaderContentState extends State { onPressed: () { fluent.Flyout.of(context).close(); saveImage( - url, _c.watchData.value?.headers, mounted, context); + url, + _c.watchData.value?.headers, + context, + ); }, ), ]); @@ -344,8 +325,8 @@ class _ComicReaderContentState extends State { title: Text('common.save'.i18n), onTap: () { Navigator.of(context).pop(); - saveImage(url, _c.watchData.value?.headers, mounted, - context); + saveImage( + url, _c.watchData.value?.headers, context); }, ), ], @@ -373,10 +354,8 @@ class _ComicReaderContentState extends State { return PlatformBuildWidget( androidBuilder: (context) { return Scaffold( - body: SafeArea( - child: _buildDisplay( - _buildContent(), - ), + body: _buildDisplay( + _buildContent(), )); }, desktopBuilder: (context) => _buildDisplay( diff --git a/lib/views/widgets/cache_network_image.dart b/lib/views/widgets/cache_network_image.dart index 8e685b79..de2c7e3d 100644 --- a/lib/views/widgets/cache_network_image.dart +++ b/lib/views/widgets/cache_network_image.dart @@ -93,8 +93,7 @@ class CacheNetWorkImagePic extends StatelessWidget { } } -void saveImage(url, Map? headers, bool mounted, - BuildContext context) async { +void saveImage(url, Map? headers, BuildContext context) async { // final url = widget.url; final fileName = url.split('/').last; final res = await dio.get( @@ -109,13 +108,14 @@ void saveImage(url, Map? headers, bool mounted, res.data, name: fileName, ); - if (mounted) { + if (context.mounted) { final msg = result['isSuccess'] == true ? 'common.save-success'.i18n : result['errorMessage']; showPlatformSnackbar( context: context, content: msg, + severity: fluent.InfoBarSeverity.success, ); } return; @@ -155,45 +155,6 @@ class _ThumnailPageState extends State<_ThumnailPage> { super.dispose(); } - // _saveImage() async { - // final url = widget.url; - // final fileName = url.split('/').last; - // final res = await dio.get( - // url, - // options: Options( - // responseType: ResponseType.bytes, - // headers: widget.headers, - // ), - // ); - // if (Platform.isAndroid) { - // final result = await ImageGallerySaver.saveImage( - // res.data, - // name: fileName, - // ); - // if (mounted) { - // final msg = result['isSuccess'] == true - // ? 'common.save-success'.i18n - // : result['errorMessage']; - // showPlatformSnackbar( - // context: context, - // content: msg, - // ); - // } - // return; - // } - // // 打开目录选择对话框file_picker - - // final path = await FilePicker.platform.saveFile( - // type: FileType.image, - // fileName: fileName, - // ); - // if (path == null) { - // return; - // } - // // 保存 - // File(path).writeAsBytesSync(res.data); - // } - Widget _buildContent(BuildContext context) { return Center( child: ExtendedImageSlidePage( @@ -250,7 +211,7 @@ class _ThumnailPageState extends State<_ThumnailPage> { title: Text('common.save'.i18n), onTap: () { Navigator.of(context).pop(); - saveImage(widget.url, widget.headers, mounted, context); + saveImage(widget.url, widget.headers, context); }, ), ], @@ -281,7 +242,7 @@ class _ThumnailPageState extends State<_ThumnailPage> { text: Text('common.save'.i18n), onPressed: () { fluent.Flyout.of(context).close(); - saveImage(widget.url, widget.headers, mounted, context); + saveImage(widget.url, widget.headers, context); }, ), ]); From 27c85612a7b66223a62885eda2cac9b12592ecff Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 25 Feb 2024 21:20:16 +0800 Subject: [PATCH 19/24] change viewing widget for comic reader (unstable ) --- lib/controllers/watch/comic_controller.dart | 128 ++++---- .../reader/comic/comic_reader_content.dart | 291 ++++++++++++------ lib/views/widgets/cache_network_image.dart | 26 +- .../widgets/watch/control_panel_footer.dart | 14 +- 4 files changed, 284 insertions(+), 175 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 1bfdb1c0..863db687 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -35,27 +35,25 @@ class ComicController extends ReaderController { final currentScale = 1.0.obs; // 当前页码 - final pageController = ExtendedPageController().obs; + final extendedPageController = ExtendedPageController().obs; + final pagecontroller = PageController(); final itemPositionsListener = ItemPositionsListener.create(); // 是否已经恢复上次阅读 final isRecover = false.obs; final readType = MangaReadMode.standard.obs; - final globalScrollController = ScrollController(); final currentOffset = 0.0.obs; final isZoom = false.obs; - final isScrollEnd = false.obs; - + final ScrollController scrollController = ScrollController(); + final RxDouble height = RxDouble(-1.0); + final RxDouble width = RxDouble(-1.0); + final StreamController>> contentStreamController = + StreamController>>(); + //用來判斷是否觸發上一頁(1)下一頁(-1),會在觸發StreamBuilder 時使用 + int pageCall = 0; @override void onInit() async { _initSetting(); getContent(); - // getTartgetContent(playIndex); - Timer.periodic(const Duration(milliseconds: 500), (timer) { - if (globalItemScrollController.isAttached) { - globalItemScrollController.jumpTo(index: index.value); - timer.cancel(); - } - }); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); enableWakeLock.value = MiruStorage.getSetting(SettingKey.enableWakelock); WakelockPlus.toggle(enable: enableWakeLock.value); @@ -70,9 +68,7 @@ class ComicController extends ReaderController { scrollOffsetListener.changes.listen((event) { hideControlPanel(); }); - // ever(height, (callback) { - // super.height.value = callback; - // }); + //300ms 偵測是否觸發控制面板 mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { if (setControllPanel.value) { isShowControlPanel.value = true; @@ -119,23 +115,12 @@ class ComicController extends ReaderController { progress.value = callback; } updateSlider.value = false; - int fullIndex = 0; - debugPrint(currentLocalProgress.value.toString()); - for (int i = 0; i < itemlength.length; i++) { - fullIndex += itemlength[i]; - if (fullIndex > callback) { - index.value = i; - super.index.value = i; - currentLocalProgress.value = callback - (fullIndex - itemlength[i]); - break; - } - } }); ever(super.watchData, (callback) async { if (isRecover.value || callback == null) { return; } - loadTargetContent(playIndex); + // loadTargetContent(playIndex); isRecover.value = true; // 获取上次阅读的页码 final history = await DatabaseService.getHistoryByPackageAndUrl( @@ -166,7 +151,6 @@ class ComicController extends ReaderController { await runtime.watch(playList[targetIndex].url); items[targetIndex] = updatedData.urls as List; itemlength[targetIndex] = updatedData.urls.length; - isScrollEnd.value = false; } catch (e) { error.value = e.toString(); } @@ -210,38 +194,39 @@ class ComicController extends ReaderController { ); } - jumpScroller(int pos) async { - if (readType.value == MangaReadMode.webTonn) { - if (globalItemScrollController.isAttached) { - globalItemScrollController.jumpTo( - index: pos, - ); - } - return; - } - } + // jumpScroller(int pos) async { + // // if (readType.value == MangaReadMode.webTonn) { + // // if (itemScrollController.isAttached && pageCall == 0) { + // // itemScrollController.scrollTo( + // // index: pos, + // // duration: const Duration(milliseconds: 300), + // // ); + // // } + // // return; + // // } + // } - _jumpPage(int page) async { + _jumpPage(int page) { if (readType.value == MangaReadMode.webTonn) { - if (itemScrollController.isAttached) { + if (itemScrollController.isAttached && pageCall == 0) { itemScrollController.jumpTo( index: page, ); } return; } - if (pageController.value.hasClients) { - pageController.value.jumpToPage(page); + if (extendedPageController.value.hasClients) { + extendedPageController.value.jumpToPage(page); return; } - pageController.value = ExtendedPageController(initialPage: page); + extendedPageController.value = ExtendedPageController(initialPage: page); } // 下一页 @override void nextPage() { if (readType.value != MangaReadMode.webTonn) { - pageController.value.nextPage( + extendedPageController.value.nextPage( duration: const Duration(milliseconds: 300), curve: Curves.ease, ); @@ -258,7 +243,7 @@ class ComicController extends ReaderController { @override void previousPage() { if (readType.value != MangaReadMode.webTonn) { - pageController.value.previousPage( + extendedPageController.value.previousPage( duration: const Duration(milliseconds: 300), curve: Curves.ease, ); @@ -273,22 +258,40 @@ class ComicController extends ReaderController { @override Future loadNextChapter() async { - await loadTargetContent(index.value + 1); + if (index.value == itemlength.length - 1) return; + index.value++; + if (items[index.value].isNotEmpty) return; + await loadTargetContent(index.value); + contentStreamController.add(items); + pageCall = 1; + currentGlobalProgress.value = 0; return; } @override Future loadPrevChapter() async { - await loadTargetContent(index.value - 1); - if (itemScrollController.isAttached) { - itemScrollController.scrollTo( - index: itemlength[index.value - 1], - duration: const Duration(milliseconds: 10)); - return; - } - if (pageController.value.hasClients) { - pageController.value.jumpToPage(itemlength[index.value - 1]); - } + if (index.value == 0) return; + index.value--; + if (items[index.value].isNotEmpty) return; + await loadTargetContent(index.value); + contentStreamController.add(items); + pageCall = -1; + currentGlobalProgress.value = itemlength[index.value] - 1; + // if (readType.value == MangaReadMode.webTonn) { + // if (itemScrollController.isAttached) { + // itemScrollController.jumpTo( + // index: itemlength[index.value] - 1, + // ); + // } + // return; + // } + // if (extendedPageController.value.hasClients) { + // extendedPageController.value.jumpToPage(itemlength[index.value] - 1); + // } + return; + // if (pageController.value.hasClients) { + // pageController.value.jumpToPage(itemlength[index.value - 1]); + // } } @override @@ -301,10 +304,7 @@ class ComicController extends ReaderController { pages.toString(), ); } - //check auto scroller is closed or not - if (autoScrollTimer != null) { - autoScrollTimer!.cancel(); - } + autoScrollTimer?.cancel(); if (MiruStorage.getSetting(SettingKey.autoTracking) && anilistID != "") { AniListProvider.editList( status: AnilistMediaListStatus.current, @@ -319,6 +319,7 @@ class ComicController extends ReaderController { if (!Platform.isAndroid) { await WindowManager.instance.setFullScreen(false); } + scrollController.dispose(); super.onClose(); } @@ -326,10 +327,13 @@ class ComicController extends ReaderController { Future getContent() async { try { error.value = ''; - watchData.value = + + final response = await runtime.watch(cuurentPlayUrl) as ExtensionMangaWatch; - itemlength[index.value] = (watchData.value as dynamic)?.urls.length; - items[index.value] = (watchData.value as dynamic)?.urls; + itemlength[index.value] = response.urls.length; + items[index.value] = response.urls; + watchData.value = response; + contentStreamController.add(items); } catch (e) { error.value = e.toString(); } diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 6b4524d1..73553e1b 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; @@ -8,7 +9,6 @@ import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/utils/log.dart'; import 'package:miru_app/views/widgets/button.dart'; import 'package:miru_app/views/widgets/cache_network_image.dart'; - import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/progress.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; @@ -30,6 +30,7 @@ class _ComicReaderContentState extends State { final List _pointer = []; final menuController = fluent.FlyoutController(); final contextAttachKey = GlobalKey(); + List _scrollitems = []; _buildPlaceholder(BuildContext context) { final width = MediaQuery.of(context).size.width; @@ -74,7 +75,7 @@ class _ComicReaderContentState extends State { if (_c.statusBarElement["reader-settings.page-indicator".i18n]! .value) ...[ Text( - "${_c.currentLocalProgress.value + 1}/${_c.itemlength[_c.index.value]}", + "${_c.currentGlobalProgress.value + 1}/${_c.itemlength[_c.index.value]}", style: const TextStyle(color: Colors.white, fontSize: 15), ), const SizedBox(width: 8) @@ -112,15 +113,39 @@ class _ComicReaderContentState extends State { ); } - Widget webtoonContent(BuildContext context) { + Widget pageViewContent(BuildContext context, int conentIndex, int initIndex) { final maxWidth = MediaQuery.of(context).size.width; final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; - final width = MediaQuery.of(context).size.width; - final height = MediaQuery.of(context).size.height; + return ExtendedImageGesturePageView.builder( + itemCount: _c.itemlength[_c.index.value], + reverse: _c.readType.value == MangaReadMode.rightToLeft, + onPageChanged: (index) { + _c.currentGlobalProgress.value = index; + }, + scrollDirection: Axis.horizontal, + controller: _c.extendedPageController.value, + itemBuilder: (BuildContext context, int index) { + final img = _c.items[_c.index.value]; + final url = img[index]; + return Container( + padding: EdgeInsets.symmetric( + horizontal: viewPadding, + ), + child: imageBuilder(url), + ); + }, + ); + } + + Widget webtoonContent(BuildContext context, int contentIndex, int initIndex) { + final maxWidth = MediaQuery.of(context).size.width; + final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; + _c.width.value = MediaQuery.of(context).size.width; + _c.height.value = MediaQuery.of(context).size.height; return Obx( () => SizedBox( - width: width, - height: height, + width: _c.width.value, + height: _c.height.value, child: Listener( onPointerDown: (event) { _pointer.add(event.pointer); @@ -136,33 +161,27 @@ class _ComicReaderContentState extends State { }, child: InteractiveViewer( scaleEnabled: _c.isZoom.value, - child: ScrollablePositionedList.builder( - physics: - _c.isZoom.value ? const NeverScrollableScrollPhysics() : null, - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - initialScrollIndex: _c.currentGlobalProgress.value, - itemScrollController: _c.itemScrollController, - itemPositionsListener: _c.itemPositionsListener, - scrollOffsetController: _c.scrollOffsetController, - scrollOffsetListener: _c.scrollOffsetListener, - itemBuilder: (context, index) { - final img = _c.items.expand((element) => element).toList(); - final url = img[index]; - SizedBox( - width: width, - height: height, - child: const Center( - child: Center( - child: ProgressRing(), - ), + child: Obx(() => ScrollablePositionedList.builder( + physics: + // const NeverScrollableScrollPhysics(), + _c.isZoom.value + ? const NeverScrollableScrollPhysics() + : null, + padding: EdgeInsets.symmetric( + horizontal: viewPadding, ), - ); - return imageBuilder(url); - }, - itemCount: _c.items.expand((element) => element).length, - ), + initialScrollIndex: initIndex, + itemScrollController: _c.itemScrollController, + itemPositionsListener: _c.itemPositionsListener, + scrollOffsetController: _c.scrollOffsetController, + scrollOffsetListener: _c.scrollOffsetListener, + itemBuilder: (context, index) { + final img = _c.items[contentIndex]; + final url = img[index]; + return imageBuilder(url); + }, + itemCount: _c.itemlength[contentIndex], + )), ), ), ), @@ -185,7 +204,7 @@ class _ComicReaderContentState extends State { width: double.infinity, child: LayoutBuilder( builder: ((context, constraints) { - final maxWidth = constraints.maxWidth; + // final maxWidth = constraints.maxWidth; return Obx(() { if (_c.error.value.isNotEmpty) { return Column( @@ -202,73 +221,99 @@ class _ComicReaderContentState extends State { ); } - // 加载中 - if (_c.watchData.value == null) { - return const Center(child: ProgressRing()); - } - - final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; + // final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; final readerType = _c.readType.value; - if (readerType == MangaReadMode.webTonn) { - return NotificationListener( - child: webtoonContent(context), - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.atEdge) { - bool isTop = metrics.pixels <= 0; - if (isTop) { - debugPrint('At the top'); - _c.loadPrevChapter(); - } else { - debugPrint('At the bottom'); - _c.loadNextChapter(); - } + return StreamBuilder>>( + stream: _c.contentStreamController.stream, + builder: (context, snapshot) { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (_c.readType.value == MangaReadMode.webTonn) { + if (!_c.scrollController.hasClients) return; + _c.scrollController.animateTo(_c.height * _c.index.value, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn); + _c.itemScrollController + .jumpTo(index: _c.currentGlobalProgress.value); + return; } - return true; - }, - ); - } - - //common mode and left to right mode - return Obx( - () => NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.atEdge) { - bool isTop = metrics.pixels <= 0; - if (isTop) { - logger.info('At the start'); - _c.loadPrevChapter(); - } else { - logger.info('At the end'); - _c.loadNextChapter(); - } + if (_c.pagecontroller.hasClients) { + _c.pagecontroller.animateToPage(_c.index.value, + duration: const Duration(milliseconds: 300), + curve: Curves.easeIn); } - // debugPrint(metrics.pixels.toString()); - return true; - }, - child: ExtendedImageGesturePageView.builder( - itemCount: _c.items.expand((element) => element).length, - reverse: readerType == MangaReadMode.rightToLeft, - onPageChanged: (index) { - _c.currentGlobalProgress.value = index; - }, - scrollDirection: Axis.horizontal, - controller: _c.pageController.value, - itemBuilder: (BuildContext context, int index) { - final img = - _c.items.expand((element) => element).toList(); - final url = img[index]; - return Container( - padding: EdgeInsets.symmetric( - horizontal: viewPadding, + if (_c.extendedPageController.value.hasClients) { + _c.extendedPageController.value + .jumpToPage(_c.currentGlobalProgress.value); + } + _c.pageCall = 0; + }); + // 加载中 + if (_c.watchData.value == null || + _c.itemlength[_c.index.value] == 0) { + return const Center(child: ProgressRing()); + } + _c.height.value = MediaQuery.of(context).size.height; + + int overrideIndex = _c.index.value; + int initScrollIndex = _c.currentGlobalProgress.value; + switch (_c.pageCall) { + //上一頁 + case -1: + overrideIndex = _c.index.value + 1; + initScrollIndex = _c.itemlength[_c.index.value] - 1; + break; + //下一頁 + case 1: + overrideIndex = _c.index.value - 1; + initScrollIndex = 0; + break; + } //_scrollitems 只有一個scrollablePositionedList or ExtendPageView 其餘都是Container + _scrollitems[overrideIndex] = Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + color: Colors.green, + child: Center( + child: Text( + overrideIndex.toString(), ), - child: imageBuilder(url), - ); - }, - ), - ), + )); + + //webtoon mode + if (readerType == MangaReadMode.webTonn) { + _scrollitems[_c.index.value] = webtoonContent( + context, _c.index.value, initScrollIndex); + return EasyRefresh( + onRefresh: () async { + logger.info("top"); + await _c.loadPrevChapter(); + }, + onLoad: () async { + await _c.loadNextChapter(); + logger.info("bottom"); + }, + child: ListView.builder( + controller: _c.scrollController, + itemCount: _scrollitems.length, + itemBuilder: (context, index) => _scrollitems[index], + )); + } + //common mode and left to right mode + _scrollitems[_c.index.value] = pageViewContent( + context, _c.index.value, _c.currentGlobalProgress.value); + + return EasyRefresh( + onRefresh: () async { + await _c.loadPrevChapter(); + }, + onLoad: () async { + await _c.loadNextChapter(); + }, + child: PageView.builder( + controller: _c.pagecontroller, + itemBuilder: (context, index) => _scrollitems[index], + )); + }, ); }); }), @@ -338,6 +383,34 @@ class _ComicReaderContentState extends State { child: CacheNetWorkImagePic( url, fit: BoxFit.fitWidth, + loadStateChanged: (state) { + if (state.extendedImageLoadState == LoadState.loading) { + if (state.loadingProgress == null) { + return SizedBox( + width: _c.width.value, + height: _c.height.value, + child: const Center( + child: ProgressRing(), + )); + } + return SizedBox( + width: _c.width.value, + height: _c.width.value, + child: Center( + child: state.loadingProgress!.expectedTotalBytes == null + ? const ProgressRing() + : ProgressRing( + value: + state.loadingProgress!.cumulativeBytesLoaded / + state.loadingProgress!.expectedTotalBytes!, + ), + )); + } + if (state.extendedImageLoadState == LoadState.completed) { + return state.completedWidget; + } + return const Center(child: Icon(fluent.FluentIcons.error)); + }, placeholder: _buildPlaceholder(context), headers: _c.watchData.value?.headers, initGestureConfigHandler: (state) { @@ -348,9 +421,28 @@ class _ComicReaderContentState extends State { )); } + @override + void initState() { + super.initState(); + } + @override Widget build(BuildContext context) { // _c.height.value = MediaQuery.of(context).size.height; + _scrollitems = List.generate(_c.itemlength.length, (index) { + if (index == _c.index.value) { + return webtoonContent( + context, _c.index.value, _c.currentGlobalProgress.value); + } + return Container( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + color: Colors.green, + child: Center( + child: Text("$index"), + ), + ); + }); return PlatformBuildWidget( androidBuilder: (context) { return Scaffold( @@ -364,3 +456,10 @@ class _ComicReaderContentState extends State { ); } } +/* +Streambuilder + | + |---(webtooon)ListView.builder->ScrollablePositionedList.builder->InteractiveViewer->ExtendedImageGesturePageView.builder + | + |---(common,reversed)PageView.builder->ExtendedImageGesturePageView.builder +*/ \ No newline at end of file diff --git a/lib/views/widgets/cache_network_image.dart b/lib/views/widgets/cache_network_image.dart index de2c7e3d..0be1be62 100644 --- a/lib/views/widgets/cache_network_image.dart +++ b/lib/views/widgets/cache_network_image.dart @@ -25,6 +25,8 @@ class CacheNetWorkImagePic extends StatelessWidget { this.canFullScreen = false, this.mode = ExtendedImageMode.none, this.initGestureConfigHandler, + this.loadStateChanged, + this.enableLoadState = true, }); final String url; final BoxFit fit; @@ -36,6 +38,8 @@ class CacheNetWorkImagePic extends StatelessWidget { final Widget? placeholder; final ExtendedImageMode mode; final InitGestureConfigHandler? initGestureConfigHandler; + final Widget? Function(ExtendedImageState)? loadStateChanged; + final bool enableLoadState; _errorBuild() { if (fallback != null) { return fallback!; @@ -53,17 +57,19 @@ class CacheNetWorkImagePic extends StatelessWidget { height: height, cache: true, mode: mode, + enableLoadState: enableLoadState, initGestureConfigHandler: initGestureConfigHandler, - loadStateChanged: (state) { - switch (state.extendedImageLoadState) { - case LoadState.loading: - return placeholder ?? const SizedBox(); - case LoadState.completed: - return state.completedWidget; - case LoadState.failed: - return _errorBuild(); - } - }, + loadStateChanged: loadStateChanged ?? + (state) { + switch (state.extendedImageLoadState) { + case LoadState.loading: + return placeholder ?? const SizedBox(); + case LoadState.completed: + return state.completedWidget; + case LoadState.failed: + return _errorBuild(); + } + }, ); if (canFullScreen) { diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index 8ee1064a..2384b7a0 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -37,7 +37,7 @@ class _ControlPanelFooterState progressObs.value = _c.progress.value; totalObs.value = _c.itemlength[_c.index.value]; }); - ever(_c.currentLocalProgress, (callback) { + ever(_c.currentGlobalProgress, (callback) { progressObs.value = callback; }); } @@ -83,8 +83,9 @@ class _ControlPanelFooterState if (totalObs.value != 0 || !_c.isShowControlPanel.value) { return Slider( - label: (_c.currentLocalProgress.value + 1) - .toString(), + label: + (_c.currentGlobalProgress.value + 1) + .toString(), max: _c.itemlength[_c.index.value] < 1 ? 1 : (_c.itemlength[_c.index.value] - 1) @@ -93,13 +94,12 @@ class _ControlPanelFooterState divisions: (totalObs.value - 1) < 0 ? 1 : totalObs.value - 1, - value: _c.currentLocalProgress.value + value: _c.currentGlobalProgress.value .toDouble(), onChanged: (val) { _c.updateSlider.value = true; - _c.progress.value = - _c.localToGloabalProgress( - val.toInt()); + _c.setControllPanel.value = true; + _c.progress.value = val.toInt(); }, ); } From e30899740bdbb5b0338c1106b8b4a177f30fa0de Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Wed, 28 Feb 2024 16:39:22 +0800 Subject: [PATCH 20/24] improve comic infinite scroll revert previous comit (27c85612a7b66223a62885eda2cac9b12592ecff) scrollable_positioned_list -> CustomScrollView --- lib/controllers/watch/comic_controller.dart | 228 +++++----- .../reader/comic/comic_reader_content.dart | 404 ++++++++---------- lib/views/widgets/cache_network_image.dart | 37 +- .../widgets/watch/control_panel_footer.dart | 15 +- pubspec.lock | 48 ++- 5 files changed, 372 insertions(+), 360 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 863db687..f9a54e63 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -7,6 +7,7 @@ import 'package:miru_app/data/providers/anilist_provider.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; import 'package:miru_app/data/services/database_service.dart'; +import 'package:miru_app/utils/log.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; import 'package:miru_app/utils/miru_storage.dart'; @@ -32,43 +33,52 @@ class ComicController extends ReaderController { }; final String setting = MiruStorage.getSetting(SettingKey.readingMode); // final readType = MangaReadMode.standard.obs; - + // final StreamController visbility = StreamController(); final currentScale = 1.0.obs; // 当前页码 - final extendedPageController = ExtendedPageController().obs; - final pagecontroller = PageController(); + final pageController = ExtendedPageController().obs; final itemPositionsListener = ItemPositionsListener.create(); // 是否已经恢复上次阅读 final isRecover = false.obs; final readType = MangaReadMode.standard.obs; + final globalScrollController = ScrollController(); final currentOffset = 0.0.obs; final isZoom = false.obs; - final ScrollController scrollController = ScrollController(); - final RxDouble height = RxDouble(-1.0); - final RxDouble width = RxDouble(-1.0); - final StreamController>> contentStreamController = - StreamController>>(); - //用來判斷是否觸發上一頁(1)下一頁(-1),會在觸發StreamBuilder 時使用 - int pageCall = 0; + final isScrollEnd = false.obs; + final positionedindex = 0.obs; + final scrollController = ScrollController(); + final RxDouble height = (-1.0).obs; + @override void onInit() async { _initSetting(); getContent(); + // getTartgetContent(playIndex); + // Timer.periodic(const Duration(milliseconds: 500), (timer) { + // // if (globalItemScrollController.isAttached) { + // // globalItemScrollController.jumpTo(index: index.value); + // // timer.cancel(); + // // } + // }); SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky); enableWakeLock.value = MiruStorage.getSetting(SettingKey.enableWakelock); WakelockPlus.toggle(enable: enableWakeLock.value); + //webtoon 模式的scrollcontroller + scrollController.addListener(_onscroll); - itemPositionsListener.itemPositions.addListener(() { - if (itemPositionsListener.itemPositions.value.isEmpty) { - return; - } - final pos = itemPositionsListener.itemPositions.value.first; - currentGlobalProgress.value = pos.index; - }); - scrollOffsetListener.changes.listen((event) { - hideControlPanel(); - }); - //300ms 偵測是否觸發控制面板 + // itemPositionsListener.itemPositions.addListener(() { + // if (itemPositionsListener.itemPositions.value.isEmpty) { + // return; + // } + // final pos = itemPositionsListener.itemPositions.value.first; + // currentGlobalProgress.value = pos.index; + // }); + // scrollOffsetListener.changes.listen((event) { + // hideControlPanel(); + // }); + // ever(height, (callback) { + // super.height.value = callback; + // }); mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { if (setControllPanel.value) { isShowControlPanel.value = true; @@ -77,7 +87,7 @@ class ComicController extends ReaderController { isShowControlPanel.value = false; }); ever(readType, (callback) { - _jumpPage(currentGlobalProgress.value); + _jumpToPage(currentGlobalProgress.value); // 保存设置 DatabaseService.setMangaReaderType( super.detailUrl, @@ -106,8 +116,9 @@ class ComicController extends ReaderController { if (!updateSlider.value) { return; } + logger.info(progress); currentGlobalProgress.value = callback; - _jumpPage(callback); + _jumpToPage(callback); }); ever(currentGlobalProgress, (callback) { @@ -115,12 +126,23 @@ class ComicController extends ReaderController { progress.value = callback; } updateSlider.value = false; + int fullIndex = 0; + // debugPrint(currentLocalProgress.value.toString()); + for (int i = 0; i < itemlength.length; i++) { + fullIndex += itemlength[i]; + if (fullIndex > callback) { + index.value = i; + super.index.value = i; + currentLocalProgress.value = callback - (fullIndex - itemlength[i]); + break; + } + } }); ever(super.watchData, (callback) async { if (isRecover.value || callback == null) { return; } - // loadTargetContent(playIndex); + loadTargetContent(playIndex); isRecover.value = true; // 获取上次阅读的页码 final history = await DatabaseService.getHistoryByPackageAndUrl( @@ -135,50 +157,63 @@ class ComicController extends ReaderController { return; } currentGlobalProgress.value = int.parse(history.progress); - _jumpPage(currentGlobalProgress.value); - // jumpScroller(index.value); + _jumpToPage(currentGlobalProgress.value); }); super.onInit(); } + void _onscroll() { + if (updateSlider.value) { + hideControlPanel(); + } + // 現在位置 + final pos = + scrollController.offset - scrollController.position.minScrollExtent; + currentGlobalProgress.value = pos ~/ height.value; + } + @override Future loadTargetContent(int targetIndex) async { try { - if (targetIndex < 0 || targetIndex == itemlength.length) { + if (targetIndex < 0 || + targetIndex == itemlength.length || + items[targetIndex].isNotEmpty) { return; } final dynamic updatedData = await runtime.watch(playList[targetIndex].url); items[targetIndex] = updatedData.urls as List; itemlength[targetIndex] = updatedData.urls.length; + isScrollEnd.value = false; } catch (e) { error.value = e.toString(); } } - onKey(RawKeyEvent event) { + onKey(KeyEvent event) { // 按下 ctrl - isZoom.value = event.isControlPressed; + isZoom.value = event.logicalKey == LogicalKeyboardKey.controlLeft || + event.logicalKey == LogicalKeyboardKey.controlRight; // 上下 - if (event.isKeyPressed(LogicalKeyboardKey.arrowUp)) { + if (event.logicalKey == LogicalKeyboardKey.arrowUp) { if (readType.value == MangaReadMode.webTonn) { return previousPage(); } } - if (event.isKeyPressed(LogicalKeyboardKey.arrowDown)) { + if (event.logicalKey == LogicalKeyboardKey.arrowDown) { if (readType.value == MangaReadMode.webTonn) { return nextPage(); } } - if (event.isKeyPressed(LogicalKeyboardKey.arrowLeft)) { + if (event.logicalKey == LogicalKeyboardKey.arrowLeft) { if (readType.value == MangaReadMode.rightToLeft) { return nextPage(); } previousPage(); } - if (event.isKeyPressed(LogicalKeyboardKey.arrowRight)) { + if (event.logicalKey == LogicalKeyboardKey.arrowRight) { if (readType.value == MangaReadMode.rightToLeft) { return previousPage(); } @@ -194,48 +229,44 @@ class ComicController extends ReaderController { ); } - // jumpScroller(int pos) async { - // // if (readType.value == MangaReadMode.webTonn) { - // // if (itemScrollController.isAttached && pageCall == 0) { - // // itemScrollController.scrollTo( - // // index: pos, - // // duration: const Duration(milliseconds: 300), - // // ); - // // } - // // return; - // // } - // } - - _jumpPage(int page) { + _jumpToPage(int page) async { if (readType.value == MangaReadMode.webTonn) { - if (itemScrollController.isAttached && pageCall == 0) { - itemScrollController.jumpTo( - index: page, - ); + // if (itemScrollController.isAttached) { + // itemScrollController.jumpTo( + // index: page, + // ); + // } + if (scrollController.hasClients) { + scrollController.jumpTo( + scrollController.position.minScrollExtent + page * height.value); } return; } - if (extendedPageController.value.hasClients) { - extendedPageController.value.jumpToPage(page); + if (pageController.value.hasClients) { + pageController.value.jumpToPage(page); return; } - extendedPageController.value = ExtendedPageController(initialPage: page); + pageController.value = ExtendedPageController(initialPage: page); } // 下一页 @override void nextPage() { if (readType.value != MangaReadMode.webTonn) { - extendedPageController.value.nextPage( + pageController.value.nextPage( duration: const Duration(milliseconds: 300), curve: Curves.ease, ); } else { - scrollOffsetController.animateScroll( - duration: const Duration(milliseconds: 100), - curve: Curves.ease, - offset: 200.0, - ); + // scrollOffsetController.animateScroll( + // duration: const Duration(milliseconds: 100), + // curve: Curves.ease, + // offset: 200.0, + // ); + scrollController.animateTo( + scrollController.offset + scrollController.position.viewportDimension, + duration: const Duration(milliseconds: 300), + curve: Curves.ease); } } @@ -243,55 +274,50 @@ class ComicController extends ReaderController { @override void previousPage() { if (readType.value != MangaReadMode.webTonn) { - extendedPageController.value.previousPage( + pageController.value.previousPage( duration: const Duration(milliseconds: 300), curve: Curves.ease, ); } else { - scrollOffsetController.animateScroll( - duration: const Duration(milliseconds: 100), - curve: Curves.ease, - offset: -200.0, - ); + scrollController.animateTo( + scrollController.offset - scrollController.position.viewportDimension, + duration: const Duration(milliseconds: 300), + curve: Curves.ease); } } @override Future loadNextChapter() async { - if (index.value == itemlength.length - 1) return; - index.value++; - if (items[index.value].isNotEmpty) return; - await loadTargetContent(index.value); - contentStreamController.add(items); - pageCall = 1; - currentGlobalProgress.value = 0; + await loadTargetContent(index.value + 1); return; } @override Future loadPrevChapter() async { - if (index.value == 0) return; - index.value--; - if (items[index.value].isNotEmpty) return; - await loadTargetContent(index.value); - contentStreamController.add(items); - pageCall = -1; - currentGlobalProgress.value = itemlength[index.value] - 1; - // if (readType.value == MangaReadMode.webTonn) { - // if (itemScrollController.isAttached) { - // itemScrollController.jumpTo( - // index: itemlength[index.value] - 1, - // ); - // } + await loadTargetContent(index.value - 1); + // if (itemScrollController.isAttached) { + // itemScrollController.scrollTo( + // index: itemlength[index.value - 1], + // duration: const Duration(milliseconds: 10)); // return; // } - // if (extendedPageController.value.hasClients) { - // extendedPageController.value.jumpToPage(itemlength[index.value] - 1); - // } - return; - // if (pageController.value.hasClients) { - // pageController.value.jumpToPage(itemlength[index.value - 1]); - // } + if (pageController.value.hasClients) { + pageController.value.jumpToPage(itemlength[index.value - 1]); + } + } + + @override + Future getContent() async { + try { + error.value = ''; + watchData.value = + await runtime.watch(cuurentPlayUrl) as ExtensionMangaWatch; + itemlength[index.value] = (watchData.value as dynamic)?.urls.length; + items[index.value] = (watchData.value as dynamic)?.urls; + positionedindex.value = index.value; + } catch (e) { + error.value = e.toString(); + } } @override @@ -300,7 +326,7 @@ class ComicController extends ReaderController { // 获取所有页数量 final pages = super.watchData.value!.urls.length; super.addHistory( - currentGlobalProgress.value.toString(), + currentLocalProgress.value.toString(), pages.toString(), ); } @@ -322,20 +348,4 @@ class ComicController extends ReaderController { scrollController.dispose(); super.onClose(); } - - @override - Future getContent() async { - try { - error.value = ''; - - final response = - await runtime.watch(cuurentPlayUrl) as ExtensionMangaWatch; - itemlength[index.value] = response.urls.length; - items[index.value] = response.urls; - watchData.value = response; - contentStreamController.add(items); - } catch (e) { - error.value = e.toString(); - } - } } diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 73553e1b..1e306199 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'package:easy_refresh/easy_refresh.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; @@ -9,9 +8,9 @@ import 'package:miru_app/utils/i18n.dart'; import 'package:miru_app/utils/log.dart'; import 'package:miru_app/views/widgets/button.dart'; import 'package:miru_app/views/widgets/cache_network_image.dart'; + import 'package:miru_app/views/widgets/platform_widget.dart'; import 'package:miru_app/views/widgets/progress.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; import 'package:based_battery_indicator/based_battery_indicator.dart'; @@ -30,8 +29,7 @@ class _ComicReaderContentState extends State { final List _pointer = []; final menuController = fluent.FlyoutController(); final contextAttachKey = GlobalKey(); - List _scrollitems = []; - + static const Key _centerKey = ValueKey('bottom-sliver-list'); _buildPlaceholder(BuildContext context) { final width = MediaQuery.of(context).size.width; final height = MediaQuery.of(context).size.height; @@ -75,7 +73,7 @@ class _ComicReaderContentState extends State { if (_c.statusBarElement["reader-settings.page-indicator".i18n]! .value) ...[ Text( - "${_c.currentGlobalProgress.value + 1}/${_c.itemlength[_c.index.value]}", + "${_c.currentLocalProgress.value + 1}/${_c.itemlength[_c.index.value]}", style: const TextStyle(color: Colors.white, fontSize: 15), ), const SizedBox(width: 8) @@ -113,78 +111,118 @@ class _ComicReaderContentState extends State { ); } - Widget pageViewContent(BuildContext context, int conentIndex, int initIndex) { - final maxWidth = MediaQuery.of(context).size.width; - final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; - return ExtendedImageGesturePageView.builder( - itemCount: _c.itemlength[_c.index.value], - reverse: _c.readType.value == MangaReadMode.rightToLeft, - onPageChanged: (index) { - _c.currentGlobalProgress.value = index; - }, - scrollDirection: Axis.horizontal, - controller: _c.extendedPageController.value, - itemBuilder: (BuildContext context, int index) { - final img = _c.items[_c.index.value]; - final url = img[index]; - return Container( - padding: EdgeInsets.symmetric( - horizontal: viewPadding, - ), - child: imageBuilder(url), - ); - }, - ); - } - - Widget webtoonContent(BuildContext context, int contentIndex, int initIndex) { - final maxWidth = MediaQuery.of(context).size.width; - final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; - _c.width.value = MediaQuery.of(context).size.width; - _c.height.value = MediaQuery.of(context).size.height; + Widget webtoonContent(BuildContext context) { + // final maxWidth = MediaQuery.of(context).size.width; + // final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; + final width = MediaQuery.of(context).size.width; + final height = MediaQuery.of(context).size.height; + _c.height.value = height; return Obx( - () => SizedBox( - width: _c.width.value, - height: _c.height.value, - child: Listener( - onPointerDown: (event) { - _pointer.add(event.pointer); - if (_pointer.length == 2) { - _c.isZoom.value = true; - } - }, - onPointerUp: (event) { - _pointer.remove(event.pointer); - if (_pointer.length == 1) { - _c.isZoom.value = false; - } - }, - child: InteractiveViewer( - scaleEnabled: _c.isZoom.value, - child: Obx(() => ScrollablePositionedList.builder( - physics: - // const NeverScrollableScrollPhysics(), - _c.isZoom.value - ? const NeverScrollableScrollPhysics() - : null, - padding: EdgeInsets.symmetric( - horizontal: viewPadding, + () { + //切成三份,中間固定在同個index(positionedindex) 之後,做出分割 + final listPrev = _c.items + .sublist(0, _c.positionedindex.value) + .reversed + .expand((element) => element.reversed) + .toList(); + final listNext = _c.items + .sublist(_c.positionedindex.value + 1) + .expand((element) => element) + .toList(); + return SizedBox( + width: width, + height: height, + child: Listener( + onPointerDown: (event) { + _pointer.add(event.pointer); + if (_pointer.length == 2) { + _c.isZoom.value = true; + } + }, + onPointerUp: (event) { + _pointer.remove(event.pointer); + if (_pointer.length == 1) { + _c.isZoom.value = false; + } + }, + child: InteractiveViewer( + minScale: .5, + scaleEnabled: _c.isZoom.value, + child: CustomScrollView( + controller: _c.scrollController, + physics: _c.isZoom.value + ? const NeverScrollableScrollPhysics() + : null, + center: _centerKey, + slivers: + // [ + // SliverList( + // delegate: SliverChildBuilderDelegate( + // (context, index) { + // final url = listPrev[index]; + // return imageBuilder(url); + // }, + // childCount: listPrev.length, + // ), + // ), + // //設為中心點 + // SliverList.builder( + // key: _centerKey, + // itemBuilder: (context, index) { + // final img = _c.items[_c.positionedindex.value]; + // final url = img[index]; + // return imageBuilder(url); + // }, + // itemCount: _c.itemlength[_c.positionedindex.value], + // ), + // SliverList( + // delegate: SliverChildBuilderDelegate( + // (context, index) { + // final url = listNext[index]; + // return imageBuilder(url); + // }, + // childCount: listNext.length, + // ), + // ) + // ] + [ + SliverFixedExtentList( + itemExtent: height, + delegate: SliverChildBuilderDelegate( + (context, index) { + final url = listPrev[index]; + return imageBuilder(url); + }, + childCount: listPrev.length, + ), ), - initialScrollIndex: initIndex, - itemScrollController: _c.itemScrollController, - itemPositionsListener: _c.itemPositionsListener, - scrollOffsetController: _c.scrollOffsetController, - scrollOffsetListener: _c.scrollOffsetListener, - itemBuilder: (context, index) { - final img = _c.items[contentIndex]; - final url = img[index]; - return imageBuilder(url); - }, - itemCount: _c.itemlength[contentIndex], - )), + //設為中心點 + SliverFixedExtentList.builder( + itemExtent: height, + key: _centerKey, + itemBuilder: (context, index) { + final img = _c.items[_c.positionedindex.value]; + final url = img[index]; + return imageBuilder(url); + }, + itemCount: _c.itemlength[_c.positionedindex.value], + ), + SliverFixedExtentList( + itemExtent: height, + delegate: SliverChildBuilderDelegate( + (context, index) { + final url = listNext[index]; + return imageBuilder(url); + }, + childCount: listNext.length, + ), + ) + ], + ), + ), ), - ), - ), + ); + }, ); } @@ -195,16 +233,17 @@ class _ComicReaderContentState extends State { } else { backgroundColor = fluent.FluentTheme.of(context).micaBackgroundColor; } - return RawKeyboardListener( + return KeyboardListener( focusNode: FocusNode(), autofocus: true, - onKey: _c.onKey, + onKeyEvent: _c.onKey, child: Container( color: backgroundColor, width: double.infinity, child: LayoutBuilder( builder: ((context, constraints) { - // final maxWidth = constraints.maxWidth; + final maxWidth = constraints.maxWidth; + return Obx(() { if (_c.error.value.isNotEmpty) { return Column( @@ -221,99 +260,73 @@ class _ComicReaderContentState extends State { ); } - // final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; + // 加载中 + if (_c.watchData.value == null) { + return const Center(child: ProgressRing()); + } + + final viewPadding = maxWidth > 800 ? ((maxWidth - 800) / 2) : 0.0; final readerType = _c.readType.value; - return StreamBuilder>>( - stream: _c.contentStreamController.stream, - builder: (context, snapshot) { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - if (_c.readType.value == MangaReadMode.webTonn) { - if (!_c.scrollController.hasClients) return; - _c.scrollController.animateTo(_c.height * _c.index.value, - duration: const Duration(milliseconds: 300), - curve: Curves.easeIn); - _c.itemScrollController - .jumpTo(index: _c.currentGlobalProgress.value); - return; - } - if (_c.pagecontroller.hasClients) { - _c.pagecontroller.animateToPage(_c.index.value, - duration: const Duration(milliseconds: 300), - curve: Curves.easeIn); + if (readerType == MangaReadMode.webTonn) { + return NotificationListener( + child: webtoonContent(context), + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels <= 0; + if (isTop) { + debugPrint('At the top'); + _c.loadPrevChapter(); + } else { + debugPrint('At the bottom'); + _c.loadNextChapter(); + } } - if (_c.extendedPageController.value.hasClients) { - _c.extendedPageController.value - .jumpToPage(_c.currentGlobalProgress.value); - } - _c.pageCall = 0; - }); - // 加载中 - if (_c.watchData.value == null || - _c.itemlength[_c.index.value] == 0) { - return const Center(child: ProgressRing()); - } - _c.height.value = MediaQuery.of(context).size.height; + return true; + }, + ); + } - int overrideIndex = _c.index.value; - int initScrollIndex = _c.currentGlobalProgress.value; - switch (_c.pageCall) { - //上一頁 - case -1: - overrideIndex = _c.index.value + 1; - initScrollIndex = _c.itemlength[_c.index.value] - 1; - break; - //下一頁 - case 1: - overrideIndex = _c.index.value - 1; - initScrollIndex = 0; - break; - } //_scrollitems 只有一個scrollablePositionedList or ExtendPageView 其餘都是Container - _scrollitems[overrideIndex] = Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - color: Colors.green, - child: Center( - child: Text( - overrideIndex.toString(), + //common mode and left to right mode + return Obx( + () => NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels <= 0; + if (isTop) { + logger.info('At the start'); + _c.loadPrevChapter(); + } else { + logger.info('At the end'); + _c.loadNextChapter(); + } + } + // debugPrint(metrics.pixels.toString()); + return true; + }, + child: ExtendedImageGesturePageView.builder( + itemCount: _c.items.expand((element) => element).length, + reverse: readerType == MangaReadMode.rightToLeft, + onPageChanged: (index) { + _c.currentGlobalProgress.value = index; + }, + scrollDirection: Axis.horizontal, + controller: _c.pageController.value, + itemBuilder: (BuildContext context, int index) { + final img = + _c.items.expand((element) => element).toList(); + final url = img[index]; + return Container( + padding: EdgeInsets.symmetric( + horizontal: viewPadding, ), - )); - - //webtoon mode - if (readerType == MangaReadMode.webTonn) { - _scrollitems[_c.index.value] = webtoonContent( - context, _c.index.value, initScrollIndex); - return EasyRefresh( - onRefresh: () async { - logger.info("top"); - await _c.loadPrevChapter(); - }, - onLoad: () async { - await _c.loadNextChapter(); - logger.info("bottom"); - }, - child: ListView.builder( - controller: _c.scrollController, - itemCount: _scrollitems.length, - itemBuilder: (context, index) => _scrollitems[index], - )); - } - //common mode and left to right mode - _scrollitems[_c.index.value] = pageViewContent( - context, _c.index.value, _c.currentGlobalProgress.value); - - return EasyRefresh( - onRefresh: () async { - await _c.loadPrevChapter(); - }, - onLoad: () async { - await _c.loadNextChapter(); - }, - child: PageView.builder( - controller: _c.pagecontroller, - itemBuilder: (context, index) => _scrollitems[index], - )); - }, + child: imageBuilder(url), + ); + }, + ), + ), ); }); }), @@ -382,35 +395,12 @@ class _ComicReaderContentState extends State { : null, child: CacheNetWorkImagePic( url, - fit: BoxFit.fitWidth, - loadStateChanged: (state) { - if (state.extendedImageLoadState == LoadState.loading) { - if (state.loadingProgress == null) { - return SizedBox( - width: _c.width.value, - height: _c.height.value, - child: const Center( - child: ProgressRing(), - )); - } - return SizedBox( - width: _c.width.value, - height: _c.width.value, - child: Center( - child: state.loadingProgress!.expectedTotalBytes == null - ? const ProgressRing() - : ProgressRing( - value: - state.loadingProgress!.cumulativeBytesLoaded / - state.loadingProgress!.expectedTotalBytes!, - ), - )); - } - if (state.extendedImageLoadState == LoadState.completed) { - return state.completedWidget; - } - return const Center(child: Icon(fluent.FluentIcons.error)); - }, + // postFrameCallback: (context) { + // RenderBox renderBox = + // context.currentContext!.findRenderObject() as RenderBox; + // logger.info('renderBox.size: ${renderBox.size}'); + // }, + fit: BoxFit.cover, placeholder: _buildPlaceholder(context), headers: _c.watchData.value?.headers, initGestureConfigHandler: (state) { @@ -421,28 +411,9 @@ class _ComicReaderContentState extends State { )); } - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { // _c.height.value = MediaQuery.of(context).size.height; - _scrollitems = List.generate(_c.itemlength.length, (index) { - if (index == _c.index.value) { - return webtoonContent( - context, _c.index.value, _c.currentGlobalProgress.value); - } - return Container( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - color: Colors.green, - child: Center( - child: Text("$index"), - ), - ); - }); return PlatformBuildWidget( androidBuilder: (context) { return Scaffold( @@ -456,10 +427,3 @@ class _ComicReaderContentState extends State { ); } } -/* -Streambuilder - | - |---(webtooon)ListView.builder->ScrollablePositionedList.builder->InteractiveViewer->ExtendedImageGesturePageView.builder - | - |---(common,reversed)PageView.builder->ExtendedImageGesturePageView.builder -*/ \ No newline at end of file diff --git a/lib/views/widgets/cache_network_image.dart b/lib/views/widgets/cache_network_image.dart index 0be1be62..3d908a62 100644 --- a/lib/views/widgets/cache_network_image.dart +++ b/lib/views/widgets/cache_network_image.dart @@ -4,7 +4,9 @@ import 'package:dio/dio.dart'; import 'package:extended_image/extended_image.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:miru_app/utils/i18n.dart'; @@ -25,8 +27,7 @@ class CacheNetWorkImagePic extends StatelessWidget { this.canFullScreen = false, this.mode = ExtendedImageMode.none, this.initGestureConfigHandler, - this.loadStateChanged, - this.enableLoadState = true, + this.postFrameCallback, }); final String url; final BoxFit fit; @@ -38,8 +39,7 @@ class CacheNetWorkImagePic extends StatelessWidget { final Widget? placeholder; final ExtendedImageMode mode; final InitGestureConfigHandler? initGestureConfigHandler; - final Widget? Function(ExtendedImageState)? loadStateChanged; - final bool enableLoadState; + final void Function(GlobalKey contextkey)? postFrameCallback; _errorBuild() { if (fallback != null) { return fallback!; @@ -49,7 +49,14 @@ class CacheNetWorkImagePic extends StatelessWidget { @override Widget build(BuildContext context) { + final contextkey = GlobalKey(); + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (postFrameCallback != null) { + postFrameCallback!(contextkey); + } + }); final image = ExtendedImage.network( + key: contextkey, url, headers: headers, fit: fit, @@ -57,19 +64,17 @@ class CacheNetWorkImagePic extends StatelessWidget { height: height, cache: true, mode: mode, - enableLoadState: enableLoadState, initGestureConfigHandler: initGestureConfigHandler, - loadStateChanged: loadStateChanged ?? - (state) { - switch (state.extendedImageLoadState) { - case LoadState.loading: - return placeholder ?? const SizedBox(); - case LoadState.completed: - return state.completedWidget; - case LoadState.failed: - return _errorBuild(); - } - }, + loadStateChanged: (state) { + switch (state.extendedImageLoadState) { + case LoadState.loading: + return placeholder ?? const SizedBox(); + case LoadState.completed: + return state.completedWidget; + case LoadState.failed: + return _errorBuild(); + } + }, ); if (canFullScreen) { diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index 2384b7a0..07f40f6d 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -37,7 +37,7 @@ class _ControlPanelFooterState progressObs.value = _c.progress.value; totalObs.value = _c.itemlength[_c.index.value]; }); - ever(_c.currentGlobalProgress, (callback) { + ever(_c.currentLocalProgress, (callback) { progressObs.value = callback; }); } @@ -83,9 +83,8 @@ class _ControlPanelFooterState if (totalObs.value != 0 || !_c.isShowControlPanel.value) { return Slider( - label: - (_c.currentGlobalProgress.value + 1) - .toString(), + label: (_c.currentLocalProgress.value + 1) + .toString(), max: _c.itemlength[_c.index.value] < 1 ? 1 : (_c.itemlength[_c.index.value] - 1) @@ -94,12 +93,14 @@ class _ControlPanelFooterState divisions: (totalObs.value - 1) < 0 ? 1 : totalObs.value - 1, - value: _c.currentGlobalProgress.value + value: _c.currentLocalProgress.value .toDouble(), onChanged: (val) { - _c.updateSlider.value = true; _c.setControllPanel.value = true; - _c.progress.value = val.toInt(); + _c.updateSlider.value = true; + _c.progress.value = + _c.localToGloabalProgress( + val.toInt()); }, ); } diff --git a/pubspec.lock b/pubspec.lock index 32008665..7e240083 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -782,6 +782,30 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + url: "https://pub.dev" + source: hosted + version: "10.0.0" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + url: "https://pub.dev" + source: hosted + version: "2.0.1" linked_scroll_controller: dependency: transitive description: @@ -818,18 +842,18 @@ packages: dependency: transitive description: name: matcher - sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.16" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" url: "https://pub.dev" source: hosted - version: "0.5.0" + version: "0.8.0" math_expressions: dependency: transitive description: @@ -914,10 +938,10 @@ packages: dependency: transitive description: name: meta - sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e + sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.0" mime: dependency: transitive description: @@ -970,10 +994,10 @@ packages: dependency: "direct main" description: name: path - sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.8.3" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -1499,6 +1523,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + url: "https://pub.dev" + source: hosted + version: "13.0.0" volume_controller: dependency: "direct main" description: From 7028d66f786ce71da5762c06470ca6a96330648b Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 3 Mar 2024 00:15:44 +0800 Subject: [PATCH 21/24] fix: image clipping --- lib/controllers/watch/comic_controller.dart | 76 +++++++--- .../reader/comic/comic_reader_content.dart | 94 ++++++++++--- .../reader/novel/novel_reader_content.dart | 131 ++++++++++-------- pubspec.lock | 8 ++ pubspec.yaml | 2 +- 5 files changed, 212 insertions(+), 99 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index f9a54e63..8c51de9f 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -8,6 +8,7 @@ import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; import 'package:miru_app/data/services/database_service.dart'; import 'package:miru_app/utils/log.dart'; +import 'package:miru_app/views/widgets/watch/playlist.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; import 'package:miru_app/utils/miru_storage.dart'; @@ -48,7 +49,8 @@ class ComicController extends ReaderController { final positionedindex = 0.obs; final scrollController = ScrollController(); final RxDouble height = (-1.0).obs; - + late final List> keys = + List.generate(playList.length, (index) => []); @override void onInit() async { _initSetting(); @@ -87,7 +89,9 @@ class ComicController extends ReaderController { isShowControlPanel.value = false; }); ever(readType, (callback) { - _jumpToPage(currentGlobalProgress.value); + if (items.isNotEmpty) { + _jumpToPage(currentGlobalProgress.value); + } // 保存设置 DatabaseService.setMangaReaderType( super.detailUrl, @@ -121,7 +125,8 @@ class ComicController extends ReaderController { _jumpToPage(callback); }); - ever(currentGlobalProgress, (callback) { + ever(currentGlobalProgress, (callback) async { + await Future.delayed(const Duration(milliseconds: 50)); if (updateSlider.value) { progress.value = callback; } @@ -167,9 +172,9 @@ class ComicController extends ReaderController { hideControlPanel(); } // 現在位置 - final pos = - scrollController.offset - scrollController.position.minScrollExtent; - currentGlobalProgress.value = pos ~/ height.value; + // final pos = + // scrollController.offset - scrollController.position.minScrollExtent; + // currentGlobalProgress.value = pos ~/ height.value; } @override @@ -184,6 +189,8 @@ class ComicController extends ReaderController { await runtime.watch(playList[targetIndex].url); items[targetIndex] = updatedData.urls as List; itemlength[targetIndex] = updatedData.urls.length; + keys[targetIndex] = + List.generate(updatedData.urls.length, (index) => GlobalKey()); isScrollEnd.value = false; } catch (e) { error.value = e.toString(); @@ -231,15 +238,24 @@ class ComicController extends ReaderController { _jumpToPage(int page) async { if (readType.value == MangaReadMode.webTonn) { + // final local = globalToLocalProgress(page); + // final localpage = local[0]; + // final chap = local[1]; + // if (keys[chap][localpage].currentContext == null) { + // return; + // } + // Scrollable.ensureVisible(keys[chap][localpage].currentContext!, + // alignment: 0.0, duration: const Duration(milliseconds: 10)); // if (itemScrollController.isAttached) { // itemScrollController.jumpTo( // index: page, // ); // } - if (scrollController.hasClients) { - scrollController.jumpTo( - scrollController.position.minScrollExtent + page * height.value); - } + // if (scrollController.hasClients) { + // scrollController.jumpTo( + // scrollController.position.minScrollExtent + page * height.value); + // } + return; } if (pageController.value.hasClients) { @@ -258,15 +274,23 @@ class ComicController extends ReaderController { curve: Curves.ease, ); } else { + if (keys[index.value][currentLocalProgress.value + 1].currentContext == + null) { + return; + } + Scrollable.ensureVisible( + keys[index.value][currentLocalProgress.value + 1].currentContext!, + alignment: 0.0, + duration: const Duration(milliseconds: 300)); // scrollOffsetController.animateScroll( // duration: const Duration(milliseconds: 100), // curve: Curves.ease, // offset: 200.0, // ); - scrollController.animateTo( - scrollController.offset + scrollController.position.viewportDimension, - duration: const Duration(milliseconds: 300), - curve: Curves.ease); + // scrollController.animateTo( + // scrollController.offset + scrollController.position.viewportDimension, + // duration: const Duration(milliseconds: 300), + // curve: Curves.ease); } } @@ -279,10 +303,18 @@ class ComicController extends ReaderController { curve: Curves.ease, ); } else { - scrollController.animateTo( - scrollController.offset - scrollController.position.viewportDimension, - duration: const Duration(milliseconds: 300), - curve: Curves.ease); + if (keys[index.value][currentLocalProgress.value - 1].currentContext == + null) { + return; + } + Scrollable.ensureVisible( + keys[index.value][currentLocalProgress.value - 1].currentContext!, + alignment: 0.0, + duration: const Duration(milliseconds: 300)); + // scrollController.animateTo( + // scrollController.offset - scrollController.position.viewportDimension, + // duration: const Duration(milliseconds: 300), + // curve: Curves.ease); } } @@ -312,8 +344,12 @@ class ComicController extends ReaderController { error.value = ''; watchData.value = await runtime.watch(cuurentPlayUrl) as ExtensionMangaWatch; - itemlength[index.value] = (watchData.value as dynamic)?.urls.length; - items[index.value] = (watchData.value as dynamic)?.urls; + itemlength[index.value] = watchData.value!.urls.length; + items[index.value] = watchData.value!.urls; + keys[index.value] = List.generate( + watchData.value!.urls.length, + (index) => GlobalKey(), + ); positionedindex.value = index.value; } catch (e) { error.value = e.toString(); diff --git a/lib/views/pages/watch/reader/comic/comic_reader_content.dart b/lib/views/pages/watch/reader/comic/comic_reader_content.dart index 1e306199..f18541ef 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; +import 'package:inview_notifier_list/inview_notifier_list.dart'; import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/comic_controller.dart'; import 'package:miru_app/utils/i18n.dart'; @@ -117,18 +118,30 @@ class _ComicReaderContentState extends State { final width = MediaQuery.of(context).size.width; final height = MediaQuery.of(context).size.height; _c.height.value = height; + final listPrev = _c.items + .sublist(0, _c.positionedindex.value) + .expand((element) => element) + .toList() + .reversed + .toList(); + final listNext = _c.items + .sublist(_c.positionedindex.value + 1) + .expand((element) => element) + .toList(); + final keyPrev = _c.keys + .sublist(0, _c.positionedindex.value) + .expand((element) => element) + .toList() + .reversed + .toList(); + final keyNext = _c.keys + .sublist(_c.positionedindex.value + 1) + .expand((element) => element) + .toList(); return Obx( () { //切成三份,中間固定在同個index(positionedindex) 之後,做出分割 - final listPrev = _c.items - .sublist(0, _c.positionedindex.value) - .reversed - .expand((element) => element.reversed) - .toList(); - final listNext = _c.items - .sublist(_c.positionedindex.value + 1) - .expand((element) => element) - .toList(); + return SizedBox( width: width, height: height, @@ -146,9 +159,13 @@ class _ComicReaderContentState extends State { } }, child: InteractiveViewer( - minScale: .5, scaleEnabled: _c.isZoom.value, - child: CustomScrollView( + child: InViewNotifierCustomScrollView( + isInViewPortCondition: (double deltaTop, double deltaBottom, + double viewPortDimension) { + return deltaTop < 0.5 * viewPortDimension && + deltaBottom > 0.5 * viewPortDimension; + }, controller: _c.scrollController, physics: _c.isZoom.value ? const NeverScrollableScrollPhysics() @@ -186,33 +203,67 @@ class _ComicReaderContentState extends State { // ) // ] [ - SliverFixedExtentList( - itemExtent: height, + SliverList( + // itemExtent: height, delegate: SliverChildBuilderDelegate( (context, index) { final url = listPrev[index]; - return imageBuilder(url); + return InViewNotifierWidget( + key: keyPrev[index], + id: (listPrev.length - index).toString(), + builder: (context, isRendered, widget) { + if (isRendered) { + // logger.info(listPrev.length - index); + _c.currentGlobalProgress.value = + listPrev.length - index; + } + return imageBuilder(url); + }); }, childCount: listPrev.length, ), ), //設為中心點 - SliverFixedExtentList.builder( - itemExtent: height, + SliverList.builder( + // itemExtent: height, key: _centerKey, itemBuilder: (context, index) { final img = _c.items[_c.positionedindex.value]; final url = img[index]; - return imageBuilder(url); + return InViewNotifierWidget( + key: _c.keys[_c.positionedindex.value][index], + id: (index + listPrev.length + 1).toString(), + builder: (context, isRendered, widget) { + if (isRendered) { + // logger.info(index + listPrev.length + 1); + _c.currentGlobalProgress.value = + index + listPrev.length; + } + return imageBuilder(url); + }); }, itemCount: _c.itemlength[_c.positionedindex.value], ), - SliverFixedExtentList( - itemExtent: height, + SliverList( + // itemExtent: height, delegate: SliverChildBuilderDelegate( (context, index) { final url = listNext[index]; - return imageBuilder(url); + return InViewNotifierWidget( + key: keyNext[index], + id: index.toString(), + builder: (context, isRendered, widget) { + if (isRendered) { + // logger.info(index + + // listPrev.length + + // _c.itemlength[_c.positionedindex.value] + + // 1); + _c.currentGlobalProgress.value = index + + listPrev.length + + _c.itemlength[_c.positionedindex.value]; + } + return imageBuilder(url); + }); }, childCount: listNext.length, ), @@ -395,12 +446,13 @@ class _ComicReaderContentState extends State { : null, child: CacheNetWorkImagePic( url, + // postFrameCallback: (context) { // RenderBox renderBox = // context.currentContext!.findRenderObject() as RenderBox; // logger.info('renderBox.size: ${renderBox.size}'); // }, - fit: BoxFit.cover, + fit: BoxFit.fitWidth, placeholder: _buildPlaceholder(context), headers: _c.watchData.value?.headers, initGestureConfigHandler: (state) { diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index ef17d0fb..62e23c95 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -56,6 +56,10 @@ class _NovelReaderContentState extends State { final maxWidth = constraints.maxWidth; // final width = maxWidth > 800 ? maxWidth / 2 : maxWidth; final height = constraints.maxHeight; + _c.height.value = height; + _c.width.value = maxWidth; + _c.padding.value = + maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; if (_c.error.value.isNotEmpty) { return SizedBox( width: double.infinity, @@ -83,66 +87,79 @@ class _NovelReaderContentState extends State { final fontSize = _c.fontSize.value; final leading = _c.leading.value; + if (_c.readType.value == NovelReadMode.scroll) { - return Center( - child: NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.atEdge) { - bool isTop = metrics.pixels <= 0; - if (isTop) { - debugPrint('At the top'); - _c.loadPrevChapter(); - } else { - debugPrint('At the bottom'); - _c.loadNextChapter(); - } - } + return StreamBuilder( + stream: _c.streamController.stream, + builder: ((context, snapshot) { + if (snapshot.hasData) { + Center( + child: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels <= 0; + if (isTop) { + debugPrint('At the top'); + _c.loadPrevChapter(); + } else { + debugPrint('At the bottom'); + _c.loadNextChapter(); + } + } - return true; - }, - child: ScrollablePositionedList.builder( - itemPositionsListener: _c.itemPositionsListener, - initialScrollIndex: _c.currentGlobalProgress.value, - itemScrollController: _c.itemScrollController, - scrollOffsetController: _c.scrollOffsetController, - padding: EdgeInsets.symmetric( - horizontal: listviewPadding, - vertical: 16, - ), - itemBuilder: (context, index) { - final localProgress = - _c.globalToLocalProgress(index); - if (localProgress[0] == 0) { - return Column(children: [ - const SizedBox( - height: 20, - ), - Text( - _c.title + _c.playList[localProgress[1]].name, - style: const TextStyle(fontSize: 26), + return true; + }, + child: ScrollablePositionedList.builder( + itemPositionsListener: _c.itemPositionsListener, + initialScrollIndex: + _c.currentGlobalProgress.value, + itemScrollController: _c.itemScrollController, + scrollOffsetController: + _c.scrollOffsetController, + padding: EdgeInsets.symmetric( + horizontal: listviewPadding, + vertical: 16, ), - const SizedBox( - height: 20, - ), - if (_c.subtitles[localProgress[1]] - .isNotEmpty) ...[ - Text( - _c.subtitles[localProgress[1]], - style: const TextStyle(fontSize: 20), - ), - const SizedBox( - height: 20, - ) - ], - _textContent(index, fontSize, leading) - ]); - } - return _textContent(index, fontSize, leading); - }, - itemCount: - _c.items.expand((element) => element).length, - )), + itemBuilder: (context, index) { + final localProgress = + _c.globalToLocalProgress(index); + if (localProgress[0] == 0) { + return Column(children: [ + const SizedBox( + height: 20, + ), + Text( + _c.title + + _c.playList[localProgress[1]].name, + style: const TextStyle(fontSize: 26), + ), + const SizedBox( + height: 20, + ), + if (_c.subtitles[localProgress[1]] + .isNotEmpty) ...[ + Text( + _c.subtitles[localProgress[1]], + style: const TextStyle(fontSize: 20), + ), + const SizedBox( + height: 20, + ) + ], + _textContent(index, fontSize, leading) + ]); + } + return _textContent(index, fontSize, leading); + }, + itemCount: _c.items + .expand((element) => element) + .length, + )), + ); + } + return const Center(child: ProgressRing()); + }), ); } diff --git a/pubspec.lock b/pubspec.lock index 7e240083..5940a6b1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -726,6 +726,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.18.1" + inview_notifier_list: + dependency: "direct main" + description: + name: inview_notifier_list + sha256: "1ca80ee39aa585e84a4b9dc1fe7211c5f64614ce3064b0007d9396073e852e14" + url: "https://pub.dev" + source: hosted + version: "3.0.0" io: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 68ac453b..d1879fa2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: screen_brightness: ^0.2.2+1 auto_orientation: ^2.3.1 dlna_dart: ^0.0.8 - + inview_notifier_list: ^3.0.0 dev_dependencies: flutter_test: sdk: flutter From 1abd6ff945b4746c8dcd673c679a903a0ec947b5 Mon Sep 17 00:00:00 2001 From: appdevelpo <56633229+appdevelpo@users.noreply.github.com> Date: Sun, 3 Mar 2024 01:06:54 +0800 Subject: [PATCH 22/24] Update novel_reader_content.dart revert novel_reader_content.dart --- .../reader/novel/novel_reader_content.dart | 131 ++++++++---------- 1 file changed, 57 insertions(+), 74 deletions(-) diff --git a/lib/views/pages/watch/reader/novel/novel_reader_content.dart b/lib/views/pages/watch/reader/novel/novel_reader_content.dart index 62e23c95..ef17d0fb 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_content.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_content.dart @@ -56,10 +56,6 @@ class _NovelReaderContentState extends State { final maxWidth = constraints.maxWidth; // final width = maxWidth > 800 ? maxWidth / 2 : maxWidth; final height = constraints.maxHeight; - _c.height.value = height; - _c.width.value = maxWidth; - _c.padding.value = - maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; if (_c.error.value.isNotEmpty) { return SizedBox( width: double.infinity, @@ -87,79 +83,66 @@ class _NovelReaderContentState extends State { final fontSize = _c.fontSize.value; final leading = _c.leading.value; - if (_c.readType.value == NovelReadMode.scroll) { - return StreamBuilder( - stream: _c.streamController.stream, - builder: ((context, snapshot) { - if (snapshot.hasData) { - Center( - child: NotificationListener( - onNotification: (notification) { - final metrics = notification.metrics; - if (metrics.atEdge) { - bool isTop = metrics.pixels <= 0; - if (isTop) { - debugPrint('At the top'); - _c.loadPrevChapter(); - } else { - debugPrint('At the bottom'); - _c.loadNextChapter(); - } - } + return Center( + child: NotificationListener( + onNotification: (notification) { + final metrics = notification.metrics; + if (metrics.atEdge) { + bool isTop = metrics.pixels <= 0; + if (isTop) { + debugPrint('At the top'); + _c.loadPrevChapter(); + } else { + debugPrint('At the bottom'); + _c.loadNextChapter(); + } + } - return true; - }, - child: ScrollablePositionedList.builder( - itemPositionsListener: _c.itemPositionsListener, - initialScrollIndex: - _c.currentGlobalProgress.value, - itemScrollController: _c.itemScrollController, - scrollOffsetController: - _c.scrollOffsetController, - padding: EdgeInsets.symmetric( - horizontal: listviewPadding, - vertical: 16, + return true; + }, + child: ScrollablePositionedList.builder( + itemPositionsListener: _c.itemPositionsListener, + initialScrollIndex: _c.currentGlobalProgress.value, + itemScrollController: _c.itemScrollController, + scrollOffsetController: _c.scrollOffsetController, + padding: EdgeInsets.symmetric( + horizontal: listviewPadding, + vertical: 16, + ), + itemBuilder: (context, index) { + final localProgress = + _c.globalToLocalProgress(index); + if (localProgress[0] == 0) { + return Column(children: [ + const SizedBox( + height: 20, ), - itemBuilder: (context, index) { - final localProgress = - _c.globalToLocalProgress(index); - if (localProgress[0] == 0) { - return Column(children: [ - const SizedBox( - height: 20, - ), - Text( - _c.title + - _c.playList[localProgress[1]].name, - style: const TextStyle(fontSize: 26), - ), - const SizedBox( - height: 20, - ), - if (_c.subtitles[localProgress[1]] - .isNotEmpty) ...[ - Text( - _c.subtitles[localProgress[1]], - style: const TextStyle(fontSize: 20), - ), - const SizedBox( - height: 20, - ) - ], - _textContent(index, fontSize, leading) - ]); - } - return _textContent(index, fontSize, leading); - }, - itemCount: _c.items - .expand((element) => element) - .length, - )), - ); - } - return const Center(child: ProgressRing()); - }), + Text( + _c.title + _c.playList[localProgress[1]].name, + style: const TextStyle(fontSize: 26), + ), + const SizedBox( + height: 20, + ), + if (_c.subtitles[localProgress[1]] + .isNotEmpty) ...[ + Text( + _c.subtitles[localProgress[1]], + style: const TextStyle(fontSize: 20), + ), + const SizedBox( + height: 20, + ) + ], + _textContent(index, fontSize, leading) + ]); + } + return _textContent(index, fontSize, leading); + }, + itemCount: + _c.items.expand((element) => element).length, + )), ); } From ed5073ec4469121295d908b1800d5bdf5432e8cf Mon Sep 17 00:00:00 2001 From: MiaoMint Date: Mon, 4 Mar 2024 00:25:41 +0800 Subject: [PATCH 23/24] Refactored comic settings view --- lib/controllers/watch/comic_controller.dart | 1 - .../reader/comic/comic_reader_settings.dart | 220 ++++++------ lib/views/widgets/cache_network_image.dart | 2 - .../widgets/watch/control_panel_footer.dart | 323 +++++------------- .../widgets/watch/control_panel_header.dart | 114 ++++--- pubspec.lock | 48 +-- 6 files changed, 262 insertions(+), 446 deletions(-) diff --git a/lib/controllers/watch/comic_controller.dart b/lib/controllers/watch/comic_controller.dart index 8c51de9f..cb2a9850 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -8,7 +8,6 @@ import 'package:miru_app/models/index.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; import 'package:miru_app/data/services/database_service.dart'; import 'package:miru_app/utils/log.dart'; -import 'package:miru_app/views/widgets/watch/playlist.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:extended_image/extended_image.dart'; import 'package:miru_app/utils/miru_storage.dart'; diff --git a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart index 64b37e05..612994cb 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -217,15 +217,16 @@ class _ComicReaderSettingsState extends State { const SizedBox(height: 16), Text('comic-settings.autoscroller-offset'.i18n), Slider( - value: _c.autoScrollOffset.value, - max: 300.0, - divisions: 30, - label: "${_c.autoScrollOffset} pixels", - onChanged: (val) { - _c.autoScrollOffset.value = val; - MiruStorage.setSetting( - SettingKey.autoScrollOffset, val); - }), + value: _c.autoScrollOffset.value, + max: 300.0, + divisions: 30, + label: "${_c.autoScrollOffset} pixels", + onChanged: (val) { + _c.autoScrollOffset.value = val; + MiruStorage.setSetting( + SettingKey.autoScrollOffset, val); + }, + ), ], )), ) @@ -236,14 +237,12 @@ class _ComicReaderSettingsState extends State { } Widget _buildDesktop(BuildContext context) { - return Obx(() => fluent.CommandBarCard( - // backgroundColor: fluent.FluentTheme.of(context).micaBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(10)), - margin: const EdgeInsets.fromLTRB(40, 20, 40, 0), - padding: const EdgeInsets.fromLTRB(0, 0, 0, 20), + return Obx( + () => fluent.Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), child: fluent.CommandBar( isCompact: true, - primaryItems: [ + primaryItems: [ CommandBarFlyOutTarget( controller: _readModeFlyout, child: fluent.IconButton( @@ -329,106 +328,109 @@ class _ComicReaderSettingsState extends State { ), const CommnadBarDivider(), CommandBarFlyOutTarget( - controller: _indicatorConfigFlyout, - child: Padding( - padding: const EdgeInsets.only(right: 8), - child: fluent.IconButton( - icon: Row(children: [ - const Icon(fluent.FluentIcons.number_field, size: 17), - const SizedBox(width: 8), - Text("comic-settings.status-bar".i18n) - ]), - onPressed: () { - _indicatorConfigFlyout.showFlyout( - builder: (context) => fluent.FlyoutContent( - constraints: - const BoxConstraints(maxWidth: 200), - child: Obx(() => Column( - mainAxisSize: MainAxisSize.min, - children: List.generate( - _c.statusBarElement.length, - (index) => fluent.FlyoutListTile( - onPressed: () { - _c - .statusBarElement[ - _c.statusBarElement - .keys - .elementAt( - index)]! - .value = - !_c - .statusBarElement[_c - .statusBarElement - .keys - .elementAt( - index)]! - .value; - }, - text: Row(children: [ - fluent.Checkbox( - checked: _c - .statusBarElement.values - .elementAt(index) - .value, - onChanged: (val) { - if (val == null) { - return; - } - _c - .statusBarElement[_c - .statusBarElement - .keys - .elementAt( - index)]! - .value = val; - }, - ), - const SizedBox(width: 8), - Text(_c.statusBarElement.keys - .elementAt(index)) - ]), - )))), - )); - }, - ))), + controller: _indicatorConfigFlyout, + child: Padding( + padding: const EdgeInsets.only(right: 8), + child: fluent.IconButton( + icon: Row(children: [ + const Icon(fluent.FluentIcons.number_field, size: 17), + const SizedBox(width: 8), + Text("comic-settings.status-bar".i18n) + ]), + onPressed: () { + _indicatorConfigFlyout.showFlyout( + builder: (context) => fluent.FlyoutContent( + constraints: const BoxConstraints(maxWidth: 200), + child: Obx( + () => Column( + mainAxisSize: MainAxisSize.min, + children: List.generate( + _c.statusBarElement.length, + (index) => fluent.FlyoutListTile( + onPressed: () { + _c + .statusBarElement[_c + .statusBarElement.keys + .elementAt(index)]! + .value = + !_c + .statusBarElement[_c + .statusBarElement.keys + .elementAt(index)]! + .value; + }, + text: Row(children: [ + fluent.Checkbox( + checked: _c.statusBarElement.values + .elementAt(index) + .value, + onChanged: (val) { + if (val == null) { + return; + } + _c + .statusBarElement[_c + .statusBarElement.keys + .elementAt(index)]! + .value = val; + }, + ), + const SizedBox(width: 8), + Text( + _c.statusBarElement.keys.elementAt(index)) + ]), + ), + ), + ), + ), + ), + ); + }, + ), + ), + ), const CommnadBarDivider(), CommandBarFlyOutTarget( controller: _indicatorAlignmentFlyout, child: Padding( - padding: const EdgeInsets.only(right: 8), - child: fluent.IconButton( - icon: Row(children: [ - const Icon(fluent.FluentIcons.align_center, size: 17), - const SizedBox(width: 8), - Text("comic-settings.indicator-alignment".i18n) - ]), - onPressed: () { - _indicatorAlignmentFlyout.showFlyout( - builder: (context) => fluent.FlyoutContent( - constraints: - const BoxConstraints(maxWidth: 200), - child: Obx(() => Column( - mainAxisSize: MainAxisSize.min, - children: List.generate( - alignMode.keys.length, - (index) => fluent.FlyoutListTile( - onPressed: () { - _c.alignMode.value = alignMode - .values - .elementAt(index); - }, - selected: _c.alignMode.value == - alignMode.values - .elementAt(index), - text: Text(alignMode.keys - .elementAt(index)), - )))), - )); - }, - )), + padding: const EdgeInsets.only(right: 8), + child: fluent.IconButton( + icon: Row(children: [ + const Icon(fluent.FluentIcons.align_center, size: 17), + const SizedBox(width: 8), + Text("comic-settings.indicator-alignment".i18n) + ]), + onPressed: () { + _indicatorAlignmentFlyout.showFlyout( + builder: (context) => fluent.FlyoutContent( + constraints: const BoxConstraints(maxWidth: 200), + child: Obx( + () => Column( + mainAxisSize: MainAxisSize.min, + children: List.generate( + alignMode.keys.length, + (index) => fluent.FlyoutListTile( + onPressed: () { + _c.alignMode.value = + alignMode.values.elementAt(index); + }, + selected: _c.alignMode.value == + alignMode.values.elementAt(index), + text: Text(alignMode.keys.elementAt(index)), + ), + ), + ), + ), + ), + ); + }, + ), + ), ), ], - ))); + ), + ), + ); } @override diff --git a/lib/views/widgets/cache_network_image.dart b/lib/views/widgets/cache_network_image.dart index 3d908a62..9a71fa32 100644 --- a/lib/views/widgets/cache_network_image.dart +++ b/lib/views/widgets/cache_network_image.dart @@ -4,9 +4,7 @@ import 'package:dio/dio.dart'; import 'package:extended_image/extended_image.dart'; import 'package:file_picker/file_picker.dart'; import 'package:fluent_ui/fluent_ui.dart' as fluent; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/widgets.dart'; import 'package:get/get.dart'; import 'package:image_gallery_saver/image_gallery_saver.dart'; import 'package:miru_app/utils/i18n.dart'; diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index 07f40f6d..8eaa0fcf 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -4,8 +4,6 @@ import 'package:get/get.dart'; import 'package:miru_app/controllers/watch/reader_controller.dart'; import 'package:miru_app/controllers/watch/novel_controller.dart'; import 'package:miru_app/views/widgets/platform_widget.dart'; -import 'package:fluent_ui/fluent_ui.dart' as fluent; -import 'package:miru_app/utils/miru_storage.dart'; class ControlPanelFooter extends StatefulWidget { const ControlPanelFooter(this.tag, {super.key}); @@ -42,259 +40,106 @@ class _ControlPanelFooterState }); } - final _desktopOffsetFlyoutController = fluent.FlyoutController(); - final _desktopIntervalFlyoutController = fluent.FlyoutController(); Widget _buildAndroid(BuildContext context) { final double width = MediaQuery.of(context).size.width; // return Container(); return Align( - alignment: const Alignment(0, 1), - child: TweenAnimationBuilder( - builder: (context, value, child) => FractionalTranslation( - translation: value, - child: Padding( - padding: const EdgeInsets.fromLTRB(15, 0, 15, 30), - child: Obx(() => Row(children: [ - const SizedBox( - height: 10, - ), - if (_c.index.value > 0) - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50), - color: containerColor, - ), - child: IconButton( - onPressed: () { - _c.prevChap(); - }, - icon: - const Icon(Icons.skip_previous_rounded))), - const Spacer(), - SizedBox( - height: 50, - width: width * 2 / 3, - child: Material( - color: containerColor, - borderRadius: BorderRadius.circular(30), - child: Obx(() { - if (totalObs.value != 0 || - !_c.isShowControlPanel.value) { - return Slider( - label: (_c.currentLocalProgress.value + 1) - .toString(), - max: _c.itemlength[_c.index.value] < 1 - ? 1 - : (_c.itemlength[_c.index.value] - 1) - .toDouble(), - min: 0, - divisions: (totalObs.value - 1) < 0 - ? 1 - : totalObs.value - 1, - value: _c.currentLocalProgress.value - .toDouble(), - onChanged: (val) { - _c.setControllPanel.value = true; - _c.updateSlider.value = true; - _c.progress.value = - _c.localToGloabalProgress( - val.toInt()); - }, - ); - } - return const Slider( - value: 0, onChanged: null); - }))), - const Spacer(), - if (_c.index.value != _c.playList.length - 1) - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(50), - color: containerColor, - ), - child: IconButton( - onPressed: () { - _c.nextChap(); - }, - icon: const Icon(Icons.skip_next_rounded))) - ])))), - duration: const Duration(milliseconds: 200), - tween: Tween( - begin: (_c.isShowControlPanel.value) - ? const Offset(0, 1) - : Offset.zero, - end: (_c.isShowControlPanel.value) - ? Offset.zero - : const Offset(0, 1.0)), - )); - } - - Widget _buildDesktop(BuildContext context) { - final double height = MediaQuery.of(context).size.height; - return Align( - alignment: const Alignment(0, 1), - child: TweenAnimationBuilder( - builder: (context, value, child) => FractionalTranslation( - translation: value, - child: Container( - color: fluent.FluentTheme.of(context) - .micaBackgroundColor - .withOpacity(0.75), - height: 80, - child: Obx(() => Column(children: [ - const SizedBox( - height: 4, - ), - Row(children: [ - const SizedBox(width: 16), - Text((progressObs.value + 1).toString()), - const SizedBox(width: 8), - Obx(() { + alignment: const Alignment(0, 1), + child: TweenAnimationBuilder( + builder: (context, value, child) => FractionalTranslation( + translation: value, + child: Padding( + padding: const EdgeInsets.fromLTRB(15, 0, 15, 30), + child: Obx( + () => Row( + children: [ + const SizedBox( + height: 10, + ), + if (_c.index.value > 0) + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: containerColor, + ), + child: IconButton( + onPressed: () { + _c.prevChap(); + }, + icon: const Icon(Icons.skip_previous_rounded), + ), + ), + const Spacer(), + SizedBox( + height: 50, + width: width * 2 / 3, + child: Material( + color: containerColor, + borderRadius: BorderRadius.circular(30), + child: Obx(() { if (totalObs.value != 0 || !_c.isShowControlPanel.value) { - return Expanded( - child: fluent.Slider( - label: (progressObs.value + 1).toString(), - max: (totalObs.value - 1) < 0 + return Slider( + label: (_c.currentLocalProgress.value + 1) + .toString(), + max: _c.itemlength[_c.index.value] < 1 ? 1 - : (totalObs.value - 1).toDouble(), + : (_c.itemlength[_c.index.value] - 1) + .toDouble(), min: 0, divisions: (totalObs.value - 1) < 0 ? 1 : totalObs.value - 1, - value: progressObs.value.toDouble(), - onChanged: _c.isShowControlPanel.value - ? (val) { - _c.updateSlider.value = true; - _c.progress.value = val.toInt(); - } - : null, - )); + value: _c.currentLocalProgress.value.toDouble(), + onChanged: (val) { + _c.setControllPanel.value = true; + _c.updateSlider.value = true; + _c.progress.value = + _c.localToGloabalProgress(val.toInt()); + }, + ); } - return const Expanded( - child: - fluent.Slider(value: 0, onChanged: null)); - }), - const SizedBox(width: 8), - Text(totalObs.value.toString()), - const SizedBox(width: 16), - ]), - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const SizedBox(width: 48), - fluent.FlyoutTarget( - controller: _desktopIntervalFlyoutController, - child: _desktopMangaPlayerButton( - 20, fluent.FluentIcons.clock, () { - _desktopIntervalFlyoutController.showFlyout( - builder: (context) => Obx(() => - fluent.FlyoutContent( - child: SizedBox( - width: 20, - height: height / 3, - child: fluent.Slider( - vertical: true, - value: _c - .autoScrollInterval - .value - .toDouble(), - max: 500.0, - divisions: 25, - label: - "${_c.autoScrollInterval} ms", - onChanged: (val) { - _c.autoScrollInterval - .value = - val.toInt(); - MiruStorage.setSetting( - SettingKey - .autoScrollInterval, - val.toInt()); - }))))); - })), - const Spacer(flex: 10), - _desktopMangaPlayerButton( - 20, fluent.FluentIcons.previous, () { - _c.prevChap(); - }), - const Spacer(), - _desktopMangaPlayerButton( - 40, - (_c.enableAutoScroll.value) - ? fluent.FluentIcons.stop - : fluent.FluentIcons.play, () { - _c.enableAutoScroll.value = - !_c.enableAutoScroll.value; - }), - const Spacer(), - _desktopMangaPlayerButton( - 20, fluent.FluentIcons.next, () { - _c.nextChap(); - }), - const Spacer(flex: 10), - fluent.FlyoutTarget( - controller: _desktopOffsetFlyoutController, - child: _desktopMangaPlayerButton( - 20, fluent.FluentIcons.padding, () { - _desktopOffsetFlyoutController.showFlyout( - builder: (context) => Obx(() => - fluent.FlyoutContent( - child: SizedBox( - width: 20, - height: height / 3, - child: fluent.Slider( - vertical: true, - value: _c - .autoScrollOffset - .value, - max: 300.0, - divisions: 30, - label: - "${_c.autoScrollOffset} pixels", - onChanged: (val) { - _c.autoScrollOffset - .value = val; - MiruStorage.setSetting( - SettingKey - .autoScrollOffset, - val); - }))))); - })), - const SizedBox(width: 48), - ]) - ])))), - duration: const Duration(milliseconds: 200), - tween: Tween( - begin: (_c.isShowControlPanel.value || _c.enableAutoScroll.value) - ? const Offset(0, 1) - : Offset.zero, - end: (_c.isShowControlPanel.value || _c.enableAutoScroll.value) - ? Offset.zero - : const Offset(0, 1.0)), - )); - } - - Widget _desktopMangaPlayerButton( - double? size, IconData icon, VoidCallback? onPressed) { - return fluent.IconButton( - style: fluent.ButtonStyle( - shape: fluent.ButtonState.resolveWith((states) => - const fluent.RoundedRectangleBorder( - borderRadius: - fluent.BorderRadius.all(fluent.Radius.circular(50))))), - onPressed: onPressed, - icon: Icon( - icon, - size: size, + return const Slider(value: 0, onChanged: null); + }))), + const Spacer(), + if (_c.index.value != _c.playList.length - 1) + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(50), + color: containerColor, + ), + child: IconButton( + onPressed: () { + _c.nextChap(); + }, + icon: const Icon(Icons.skip_next_rounded), + ), + ) + ], + ), + ), + ), + ), + duration: const Duration(milliseconds: 200), + tween: Tween( + begin: + (_c.isShowControlPanel.value) ? const Offset(0, 1) : Offset.zero, + end: (_c.isShowControlPanel.value) + ? Offset.zero + : const Offset(0, 1.0), + ), ), ); } + Widget _buildDesktop(BuildContext context) { + return const SizedBox.shrink(); + } + @override Widget build(BuildContext context) { return PlatformBuildWidget( diff --git a/lib/views/widgets/watch/control_panel_header.dart b/lib/views/widgets/watch/control_panel_header.dart index 3558814d..d090da88 100644 --- a/lib/views/widgets/watch/control_panel_header.dart +++ b/lib/views/widgets/watch/control_panel_header.dart @@ -120,65 +120,69 @@ class _ControlPanelHeaderState color: fluent.FluentTheme.of(context).micaBackgroundColor, padding: const EdgeInsets.only(left: 16), child: MouseRegion( - onHover: (detail) { - _c.setControllPanel.value = true; - }, - child: DragToMoveArea( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - fluent.IconButton( - icon: const Icon(fluent.FluentIcons.back), - onPressed: () { - RouterUtils.pop(); - }, - ), - const SizedBox(width: 16), - Text(_c.title + _c.playList[_c.index.value].name), - const Spacer(), - // const SizedBox(width: 8), - fluent.FlyoutTarget( - controller: _playListFlayoutcontroller, - child: fluent.IconButton( - icon: const Icon(fluent.FluentIcons.collapse_menu), - onPressed: () { - _playListFlayoutcontroller.showFlyout( - builder: (context) { - return SizedBox( - width: 300, - child: Obx( - () => PlayList( - title: _c.title, - list: _c.playList.map((e) => e.name).toList(), - selectIndex: _c.index.value, - onChange: (value) { - _c.clearData(); - _c.index.value = value; - _c.getContent(); - router.pop(); - }, - ), - ), - ); - }); - }, - ), + onHover: (detail) { + _c.setControllPanel.value = true; + }, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + fluent.IconButton( + icon: const Icon(fluent.FluentIcons.back), + onPressed: () { + RouterUtils.pop(); + }, + ), + const SizedBox(width: 16), + Expanded( + child: DragToMoveArea( + child: Text( + _c.title + _c.playList[_c.index.value].name, + overflow: TextOverflow.ellipsis, ), - SizedBox( - width: 138, - child: WindowCaption( - backgroundColor: Colors.transparent, - brightness: fluent.FluentTheme.of(context).brightness, - ), - ) - ], + ), + ), + fluent.FlyoutTarget( + controller: _playListFlayoutcontroller, + child: fluent.IconButton( + icon: const Icon(fluent.FluentIcons.collapse_menu), + onPressed: () { + _playListFlayoutcontroller.showFlyout(builder: (context) { + return SizedBox( + width: 300, + child: Obx( + () => PlayList( + title: _c.title, + list: _c.playList.map((e) => e.name).toList(), + selectIndex: _c.index.value, + onChange: (value) { + _c.clearData(); + _c.index.value = value; + _c.getContent(); + router.pop(); + }, + ), + ), + ); + }); + }, + ), ), - )), + SizedBox( + width: 138, + child: WindowCaption( + backgroundColor: Colors.transparent, + brightness: fluent.FluentTheme.of(context).brightness, + ), + ) + ], + ), + ), ), fluent.Container( - height: 70, - color: fluent.FluentTheme.of(context).micaBackgroundColor, - child: widget.buildSettings(context)), + height: 60, + color: fluent.FluentTheme.of(context).micaBackgroundColor, + child: widget.buildSettings(context), + ), // Obx()) ]).animate().fade(), ); diff --git a/pubspec.lock b/pubspec.lock index 5940a6b1..13287b01 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -790,30 +790,6 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" linked_scroll_controller: dependency: transitive description: @@ -850,18 +826,18 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" math_expressions: dependency: transitive description: @@ -946,10 +922,10 @@ packages: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.10.0" mime: dependency: transitive description: @@ -1002,10 +978,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_drawing: dependency: transitive description: @@ -1531,14 +1507,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 - url: "https://pub.dev" - source: hosted - version: "13.0.0" volume_controller: dependency: "direct main" description: From 31c3c3ab6d3a339b49138adafcc95bba87fccc35 Mon Sep 17 00:00:00 2001 From: MiaoMint Date: Mon, 4 Mar 2024 08:34:15 +0800 Subject: [PATCH 24/24] Update function to handle edge cases --- .../reader/novel/novel_reader_settings.dart | 526 +++++++++--------- 1 file changed, 261 insertions(+), 265 deletions(-) diff --git a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart index 02e12aea..8a84b8c8 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart @@ -373,286 +373,282 @@ class _NovelReaderSettingsState extends State { } Widget _buildDesktop(BuildContext context) { - return Obx(() => fluent.CommandBarCard( - // backgroundColor: fluent.FluentTheme.of(context).micaBackgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(10)), - margin: const EdgeInsets.fromLTRB(40, 20, 40, 0), - padding: const EdgeInsets.fromLTRB(0, 0, 0, 20), - child: fluent.CommandBar( - isCompact: true, - primaryItems: [ - const CommandBarSpacer(width: 16), - fluent.CommandBarBuilderItem( - builder: (context, mode, w) => fluent.Tooltip( - message: "comic-settings.read-mode".i18n, child: w), - wrappedItem: CommandBarDropDownButton( - leading: const Icon( - fluent.FluentIcons.reading_mode, - size: 17, - ), - items: _c.readmode.keys - .map((e) => fluent.MenuFlyoutItem( - text: Text(e), - leading: _c.readType.value == _c.readmode[e]! - ? const Icon(fluent.FluentIcons.location_dot) - : null, - onPressed: () { - _c.readType.value = _c.readmode[e]!; - })) - .toList())), - const CommandBarSpacer(), - const CommnadBarDivider(), - fluent.CommandBarBuilderItem( - wrappedItem: fluent.CommandBarButton( - label: SizedBox( - width: 40, - child: fluent.NumberBox( - max: _c.watchData.value?.content.length ?? 1, - min: 1, - mode: fluent.SpinButtonPlacementMode.none, - clearButton: false, - value: _c.progress.value + 1, - onChanged: (value) { - if (value != null) { - _c.updateSlider.value = true; - _c.progress.value = value - 1; - } - }, - )), - onPressed: null, - ), - builder: (context, mode, w) => fluent.Tooltip( - message: "comic-settings.page".i18n, - child: w, - )), - CommandBarText( - text: "/ ${_c.watchData.value?.content.length ?? 0}"), - const CommnadBarDivider(), - fluent.CommandBarBuilderItem( + return Obx( + () => fluent.CommandBar( + isCompact: true, + primaryItems: [ + const CommandBarSpacer(width: 16), + fluent.CommandBarBuilderItem( builder: (context, mode, w) => fluent.Tooltip( - message: "reader-settings.enable-wakelock".i18n, - child: w, + message: "comic-settings.read-mode".i18n, child: w), + wrappedItem: CommandBarDropDownButton( + leading: const Icon( + fluent.FluentIcons.reading_mode, + size: 17, + ), + items: _c.readmode.keys + .map((e) => fluent.MenuFlyoutItem( + text: Text(e), + leading: _c.readType.value == _c.readmode[e]! + ? const Icon(fluent.FluentIcons.location_dot) + : null, + onPressed: () { + _c.readType.value = _c.readmode[e]!; + })) + .toList())), + const CommandBarSpacer(), + const CommnadBarDivider(), + fluent.CommandBarBuilderItem( + wrappedItem: fluent.CommandBarButton( + label: SizedBox( + width: 40, + child: fluent.NumberBox( + max: _c.watchData.value?.content.length ?? 1, + min: 1, + mode: fluent.SpinButtonPlacementMode.none, + clearButton: false, + value: _c.progress.value + 1, + onChanged: (value) { + if (value != null) { + _c.updateSlider.value = true; + _c.progress.value = value - 1; + } + }, + )), + onPressed: null, ), - wrappedItem: CommandBarToggleButton( - onchange: (val) { - _c.enableWakeLock.value = val; - WakelockPlus.toggle(enable: val); - MiruStorage.setSetting(SettingKey.enableWakelock, val); - }, - checked: _c.enableWakeLock.value, - child: - const Icon(fluent.FluentIcons.coffee_script, size: 17)), - ), - const CommnadBarDivider(), - fluent.CommandBarBuilderItem( builder: (context, mode, w) => fluent.Tooltip( - message: "reader-settings.enable-fullScreen".i18n, - child: w, - ), - wrappedItem: CommandBarToggleButton( - onchange: (val) async { - _c.enableFullScreen.value = val; - await windowManager.setFullScreen(val); - }, - checked: _c.enableFullScreen.value, - child: const Icon(fluent.FluentIcons.full_screen, size: 17)), + message: "comic-settings.page".i18n, + child: w, + )), + CommandBarText(text: "/ ${_c.watchData.value?.content.length ?? 0}"), + const CommnadBarDivider(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: "reader-settings.enable-wakelock".i18n, + child: w, ), - const CommnadBarDivider(), - const CommandBarSpacer(), - fluent.CommandBarBuilderItem( - builder: (context, mode, w) => fluent.Tooltip( - message: ("comic-settings.status-bar".i18n), - child: w, - ), - wrappedItem: CommandBarDropDownButton( - leading: - const Icon(fluent.FluentIcons.number_field, size: 17), - items: _c.statusBarElement.keys + wrappedItem: CommandBarToggleButton( + onchange: (val) { + _c.enableWakeLock.value = val; + WakelockPlus.toggle(enable: val); + MiruStorage.setSetting(SettingKey.enableWakelock, val); + }, + checked: _c.enableWakeLock.value, + child: const Icon(fluent.FluentIcons.coffee_script, size: 17)), + ), + const CommnadBarDivider(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: "reader-settings.enable-fullScreen".i18n, + child: w, + ), + wrappedItem: CommandBarToggleButton( + onchange: (val) async { + _c.enableFullScreen.value = val; + await windowManager.setFullScreen(val); + }, + checked: _c.enableFullScreen.value, + child: const Icon(fluent.FluentIcons.full_screen, size: 17)), + ), + const CommnadBarDivider(), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: ("comic-settings.status-bar".i18n), + child: w, + ), + wrappedItem: CommandBarDropDownButton( + leading: const Icon(fluent.FluentIcons.number_field, size: 17), + items: _c.statusBarElement.keys + .map((e) => fluent.MenuFlyoutItem( + leading: fluent.Checkbox( + checked: _c.statusBarElement[e]!.value, + onChanged: (val) { + if (val == null) { + return; + } + _c.statusBarElement[e]!.value = val; + }, + ), + text: Text(e), + onPressed: () { + _c.statusBarElement[e]!.value = + !_c.statusBarElement[e]!.value; + })) + .toList(), + )), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (conetx, mode, w) => fluent.Tooltip( + message: "comic-settings.indicator-alignment".i18n, + child: w, + ), + wrappedItem: CommandBarDropDownButton( + items: alignMode.keys .map((e) => fluent.MenuFlyoutItem( - leading: fluent.Checkbox( - checked: _c.statusBarElement[e]!.value, - onChanged: (val) { - if (val == null) { - return; - } - _c.statusBarElement[e]!.value = val; - }, - ), + leading: _c.alignMode.value == alignMode[e]! + ? const Icon(fluent.FluentIcons.location_dot) + : null, text: Text(e), onPressed: () { - _c.statusBarElement[e]!.value = - !_c.statusBarElement[e]!.value; + _c.alignMode.value = alignMode[e]!; })) .toList(), - )), - const CommandBarSpacer(), - fluent.CommandBarBuilderItem( - builder: (conetx, mode, w) => fluent.Tooltip( - message: "comic-settings.indicator-alignment".i18n, - child: w, - ), - wrappedItem: CommandBarDropDownButton( - items: alignMode.keys - .map((e) => fluent.MenuFlyoutItem( - leading: _c.alignMode.value == alignMode[e]! - ? const Icon(fluent.FluentIcons.location_dot) - : null, - text: Text(e), - onPressed: () { - _c.alignMode.value = alignMode[e]!; - })) - .toList(), - leading: - const Icon(fluent.FluentIcons.align_center, size: 17))), - const CommandBarSpacer(), - const CommnadBarDivider(), - const CommandBarSpacer(), - fluent.CommandBarBuilderItem( - builder: (context, displayMode, w) => fluent.Tooltip( - message: "novel-settings.highlight-text-color".i18n, - child: w, + leading: + const Icon(fluent.FluentIcons.align_center, size: 17))), + const CommandBarSpacer(), + const CommnadBarDivider(), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (context, displayMode, w) => fluent.Tooltip( + message: "novel-settings.highlight-text-color".i18n, + child: w, + ), + wrappedItem: CommandBarDropDownButton( + title: Stack(children: [ + const Icon( + fluent.FluentIcons.fabric_text_highlight, + size: 17, + // color: _c.heighLightTextColor.value, ), - wrappedItem: CommandBarDropDownButton( - title: Stack(children: [ - const Icon( - fluent.FluentIcons.fabric_text_highlight, - size: 17, - // color: _c.heighLightTextColor.value, - ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Container( - width: 17, - height: 3, - color: _c.highLightTextColor.value, - )) - ]), - items: ColorUtils.baseColors - .map((e) => fluent.MenuFlyoutItem( - text: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: e, - ), - child: e == Colors.transparent - ? const Icon( - fluent.FluentIcons.clear, - size: 17, - ) - : null, + Padding( + padding: const EdgeInsets.only(top: 20), + child: Container( + width: 17, + height: 3, + color: _c.highLightTextColor.value, + )) + ]), + items: ColorUtils.baseColors + .map((e) => fluent.MenuFlyoutItem( + text: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e, ), - onPressed: () { - _c.highLightTextColor.value = e; - })) - .toList())), - const CommandBarSpacer(), - fluent.CommandBarBuilderItem( - builder: (context, displayMode, w) => fluent.Tooltip( - message: "novel-settings.text-color".i18n, - child: w, + child: e == Colors.transparent + ? const Icon( + fluent.FluentIcons.clear, + size: 17, + ) + : null, + ), + onPressed: () { + _c.highLightTextColor.value = e; + })) + .toList())), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (context, displayMode, w) => fluent.Tooltip( + message: "novel-settings.text-color".i18n, + child: w, + ), + wrappedItem: CommandBarDropDownButton( + title: Stack(children: [ + const Icon( + fluent.FluentIcons.font_color_a, + size: 17, + // color: _c.textColor.value, ), - wrappedItem: CommandBarDropDownButton( - title: Stack(children: [ - const Icon( - fluent.FluentIcons.font_color_a, - size: 17, - // color: _c.textColor.value, - ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Container( - width: 17, - height: 3, - color: _c.textColor.value, - )) - ]), - items: ColorUtils.baseColors - .map((e) => fluent.MenuFlyoutItem( - text: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: e, - ), - child: e == Colors.transparent - ? const Icon( - fluent.FluentIcons.clear, - size: 17, - ) - : null, + Padding( + padding: const EdgeInsets.only(top: 20), + child: Container( + width: 17, + height: 3, + color: _c.textColor.value, + )) + ]), + items: ColorUtils.baseColors + .map((e) => fluent.MenuFlyoutItem( + text: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e, ), - onPressed: () { - _c.textColor.value = e; - })) - .toList())), - const CommandBarSpacer(), - fluent.CommandBarBuilderItem( - builder: (context, mode, w) => fluent.Tooltip( - message: "novel-settings.highlight-color".i18n, - child: w, + child: e == Colors.transparent + ? const Icon( + fluent.FluentIcons.clear, + size: 17, + ) + : null, + ), + onPressed: () { + _c.textColor.value = e; + })) + .toList())), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (context, mode, w) => fluent.Tooltip( + message: "novel-settings.highlight-color".i18n, + child: w, + ), + wrappedItem: CommandBarDropDownButton( + title: Stack(children: [ + const Icon( + fluent.FluentIcons.highlight, + size: 17, + // color: _c.highLightColor.value, ), - wrappedItem: CommandBarDropDownButton( - title: Stack(children: [ - const Icon( - fluent.FluentIcons.highlight, - size: 17, - // color: _c.highLightColor.value, - ), - Padding( - padding: const EdgeInsets.only(top: 20), - child: Container( - width: 17, - height: 3, - color: _c.highLightColor.value, - )) - ]), - items: ColorUtils.baseColors - .map((e) => fluent.MenuFlyoutItem( - text: Container( - width: 20, - height: 20, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: e, - ), - child: e == Colors.transparent - ? const Icon( - fluent.FluentIcons.clear, - size: 17, - ) - : null, + Padding( + padding: const EdgeInsets.only(top: 20), + child: Container( + width: 17, + height: 3, + color: _c.highLightColor.value, + )) + ]), + items: ColorUtils.baseColors + .map((e) => fluent.MenuFlyoutItem( + text: Container( + width: 20, + height: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: e, ), - onPressed: () { - _c.highLightColor.value = e; - })) - .toList())), - const CommandBarSpacer(), - const CommnadBarDivider(), - const CommandBarSpacer(), - fluent.CommandBarBuilderItem( - builder: (contex, mode, w) => fluent.Tooltip( - message: "novel-settings.font-size".i18n, - child: w, - ), - wrappedItem: CommandBarNumberBox( - onchange: (value) { - if (value != null) { - _c.fontSize.value = value; - MiruStorage.setSetting(SettingKey.novelFontSize, value); - } - }, - value: _c.fontSize.value, - min: 1, - max: 30, - title: const Icon( - fluent.FluentIcons.font_size, - size: 17, - ))) - ], - ))); + child: e == Colors.transparent + ? const Icon( + fluent.FluentIcons.clear, + size: 17, + ) + : null, + ), + onPressed: () { + _c.highLightColor.value = e; + })) + .toList())), + const CommandBarSpacer(), + const CommnadBarDivider(), + const CommandBarSpacer(), + fluent.CommandBarBuilderItem( + builder: (contex, mode, w) => fluent.Tooltip( + message: "novel-settings.font-size".i18n, + child: w, + ), + wrappedItem: CommandBarNumberBox( + onchange: (value) { + if (value != null) { + _c.fontSize.value = value; + MiruStorage.setSetting(SettingKey.novelFontSize, value); + } + }, + value: _c.fontSize.value, + min: 1, + max: 30, + title: const Icon( + fluent.FluentIcons.font_size, + size: 17, + ), + ), + ) + ], + ), + ); } @override