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 63061491..8ebabead 100644 --- a/assets/i18n/en.json +++ b/assets/i18n/en.json @@ -48,6 +48,8 @@ "no-data": "No data", "clear": "Clear", "export": "Export", + "tts":"Text to speech", + "common": "Common", "off": "Off", "error-message": "Error message", "disconnect": "Disconnect" @@ -226,10 +228,41 @@ "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", + "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", + "battery": "Battery", + "time": "Time", + "page-indicator": "Page Indicator", + "battery-icon": "Battery Icon", + "indicator-alignment": "Indicator Alignment", + "enable-fullScreen": "Enable Full Screen" }, "novel-settings": { - "font-size": "Font size" + "font-size": "Font size", + "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", + "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 fb6bfc41..cb2a9850 100644 --- a/lib/controllers/watch/comic_controller.dart +++ b/lib/controllers/watch/comic_controller.dart @@ -1,13 +1,19 @@ +import 'dart:io'; 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'; 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'; +import 'dart:async'; +import 'package:wakelock_plus/wakelock_plus.dart'; +import 'package:window_manager/window_manager.dart'; class ComicController extends ReaderController { ComicController({ @@ -26,52 +32,121 @@ 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 StreamController visbility = StreamController(); 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 isRecover = false.obs; - - // 是否按下 ctrl - + final readType = MangaReadMode.standard.obs; + final globalScrollController = ScrollController(); + final currentOffset = 0.0.obs; final isZoom = false.obs; - + final isScrollEnd = false.obs; + 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() { + void onInit() async { _initSetting(); - itemPositionsListener.itemPositions.addListener(() { - if (itemPositionsListener.itemPositions.value.isEmpty) { + 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(); + // }); + // ever(height, (callback) { + // super.height.value = callback; + // }); + mouseTimer = Timer.periodic(const Duration(milliseconds: 300), (timer) { + if (setControllPanel.value) { + isShowControlPanel.value = true; return; } - final pos = itemPositionsListener.itemPositions.value.first; - currentPage.value = pos.index; + isShowControlPanel.value = false; }); - ever(readType, (callback) { - _jumpPage(currentPage.value); + if (items.isNotEmpty) { + _jumpToPage(currentGlobalProgress.value); + } // 保存设置 DatabaseService.setMangaReaderType( super.detailUrl, callback, ); }); - // 如果切换章节,重置当前页码 - ever(super.index, (callback) => currentPage.value = 0); + 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) { + // 防止逆向回饋 + if (!updateSlider.value) { + return; + } + logger.info(progress); + currentGlobalProgress.value = callback; + _jumpToPage(callback); + }); + + ever(currentGlobalProgress, (callback) async { + await Future.delayed(const Duration(milliseconds: 50)); + 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( @@ -85,35 +160,66 @@ class ComicController extends ReaderController { history.episodeId != index.value) { return; } - currentPage.value = int.parse(history.progress); - _jumpPage(currentPage.value); + currentGlobalProgress.value = int.parse(history.progress); + _jumpToPage(currentGlobalProgress.value); }); super.onInit(); } - onKey(RawKeyEvent event) { + 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 || + items[targetIndex].isNotEmpty) { + return; + } + final dynamic updatedData = + 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(); + } + } + + 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(); } @@ -129,13 +235,26 @@ class ComicController extends ReaderController { ); } - _jumpPage(int page) async { + _jumpToPage(int page) async { if (readType.value == MangaReadMode.webTonn) { - if (itemScrollController.isAttached) { - itemScrollController.jumpTo( - index: page, - ); - } + // 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); + // } + return; } if (pageController.value.hasClients) { @@ -154,11 +273,23 @@ class ComicController extends ReaderController { curve: Curves.ease, ); } else { - scrollOffsetController.animateScroll( - duration: const Duration(milliseconds: 100), - curve: Curves.ease, - offset: 200.0, - ); + 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); } } @@ -171,24 +302,70 @@ class ComicController extends ReaderController { curve: Curves.ease, ); } else { - scrollOffsetController.animateScroll( - duration: const Duration(milliseconds: 100), - curve: Curves.ease, - offset: -200.0, + 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); + } + } + + @override + Future loadNextChapter() async { + await loadTargetContent(index.value + 1); + 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]); + } + } + + @override + Future getContent() async { + try { + error.value = ''; + watchData.value = + await runtime.watch(cuurentPlayUrl) as ExtensionMangaWatch; + 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(); } } @override - void onClose() { + void onClose() async { if (super.watchData.value != null) { // 获取所有页数量 final pages = super.watchData.value!.urls.length; super.addHistory( - currentPage.value.toString(), + currentLocalProgress.value.toString(), pages.toString(), ); } + autoScrollTimer?.cancel(); if (MiruStorage.getSetting(SettingKey.autoTracking) && anilistID != "") { AniListProvider.editList( status: AnilistMediaListStatus.current, @@ -196,6 +373,14 @@ class ComicController extends ReaderController { mediaId: anilistID, ); } + + mouseTimer?.cancel(); + WakelockPlus.disable(); + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + if (!Platform.isAndroid) { + await WindowManager.instance.setFullScreen(false); + } + scrollController.dispose(); super.onClose(); } } diff --git a/lib/controllers/watch/novel_controller.dart b/lib/controllers/watch/novel_controller.dart index 27c760ef..a2e15841 100644 --- a/lib/controllers/watch/novel_controller.dart +++ b/lib/controllers/watch/novel_controller.dart @@ -1,9 +1,22 @@ +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'; 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'; +import 'package:flutter_tts/flutter_tts.dart'; +import 'package:flutter/material.dart'; + +enum NovelReadMode { + singlePage, + doublePage, + scroll, +} class NovelController extends ReaderController { NovelController({ @@ -19,59 +32,264 @@ 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; - final positions = 0.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; + 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; + 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"); + }); + } @override - void onInit() { + void onInit() async { super.onInit(); + getContent(); + 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)); 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(); + }); + 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; + } }); - ever( - fontSize, - (callback) => MiruStorage.setSetting(SettingKey.novelFontSize, callback), - ); + ever(bookPage, (callback) { + if (callback == -1) { + return; + } + //只處理單頁模式 + }); // 切换章节时重置页码 - 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; + } + currentGlobalProgress.value = int.parse(history.progress); + _jumpLine(currentGlobalProgress.value); + }); + ever(progress, (callback) { + // 防止逆向回饋 + if (!updateSlider.value) { + return; + } + 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) - 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) { + 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); }); } + _jumpLine(int? index) { + if (index == null) { + return; + } + itemScrollController.jumpTo(index: index); + } + + animeScrollTo(index) { + itemScrollController.scrollTo( + index: index, + duration: const Duration(milliseconds: 10), + ); + } + @override void onClose() { if (super.watchData.value != null) { final totalProgress = watchData.value!.content.length.toString(); super.addHistory( - positions.value.toString(), + currentGlobalProgress.value.toString(), totalProgress, ); } + SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); + flutterTts.stop(); + mouseTimer?.cancel(); + bookController.dispose(); super.onClose(); } + + //獲取目標章節內容,但不更新當前頁面 + @override + 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; + subtitles[targetIndex] = updatedData.subtitle ?? ''; + } catch (e) { + error.value = e.toString(); + } + } + + void setReadingPage(int page) { + bookPage.value = page; + } + + @override + Future loadNextChapter() async { + await loadTargetContent(index.value + 1); + 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; + } + } + + @override + 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; + 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 97bac28b..03253e4f 100644 --- a/lib/controllers/watch/reader_controller.dart +++ b/lib/controllers/watch/reader_controller.dart @@ -1,13 +1,17 @@ import 'dart:async'; - import 'package:get/get.dart'; -import 'package:miru_app/models/extension.dart'; -import 'package:miru_app/models/history.dart'; +import 'package:flutter/material.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'; +import 'package:miru_app/utils/i18n.dart'; +import 'package:battery_plus/battery_plus.dart'; +import 'package:screen_brightness/screen_brightness.dart'; -class ReaderController extends GetxController { +abstract class ReaderController extends GetxController { final String title; final List playList; final String detailUrl; @@ -16,7 +20,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, @@ -30,38 +36,151 @@ 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; get cuurentPlayUrl => playList[index.value].url; - Timer? _timer; + Timer? autoScrollTimer; + final isScrolled = true.obs; + final updateSlider = true.obs; + final isInfinityScrollMode = false.obs; + Timer? _barreryTimer; + //點擊區域是否反轉 + 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; + final RxBool isMouseHover = false.obs; + final RxBool setControllPanel = false.obs; + Timer? mouseTimer; + final RxBool enableWakeLock = false.obs; + final RxBool enableFullScreen = false.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; + final RxBool enableTapRegion = true.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; + final RxDouble brightness = 0.5.obs; @override - void onInit() { - getContent(); - ever(index, (callback) => getContent()); + void onInit() async { + // getContent(); + autoScrollInterval.value = _autoScrollInterval; + autoScrollOffset.value = _autoScrollOffset; + nextPageHitBox.value = _nextPageHitBox; + prevPageHitBox.value = _prevPageHitBox; + await _statusBar(); + await _currentBrightness(); + _barreryTimer = + Timer.periodic(const Duration(seconds: 10), (timer) => _statusBar()); + super.onInit(); } - getContent() async { + Future _currentBrightness() async { try { - error.value = ''; - watchData.value = null; - watchData.value = await runtime.watch(cuurentPlayUrl) as T; + brightness.value = await ScreenBrightness().current; } catch (e) { - error.value = e.toString(); + throw 'Failed to get current brightness'; } } - void previousPage() {} + 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++) { + progress += itemlength[i]; + } + progress = localProgress.toInt() + progress; + return progress; + } + + 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; + } + } + return [localProgress, chapter]; + } + void previousPage() {} void nextPage() {} + void loadNextChapter(); + void loadPrevChapter(); + + void nextChap() { + clearData(); + index.value++; + getContent(); + } - showControlPanel() { - isShowControlPanel.value = true; - _timer?.cancel(); - _timer = Timer(const Duration(seconds: 3), () { - isShowControlPanel.value = false; - }); + 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; + } + + hideControlPanel() { + setControllPanel.value = false; } addHistory(String progress, String totalProgress) async { @@ -80,4 +199,12 @@ class ReaderController extends GetxController { ); await Get.find().onRefresh(); } + + @override + void onClose() { + _barreryTimer?.cancel(); + autoScrollTimer?.cancel(); + mouseTimer?.cancel(); + super.onClose(); + } } diff --git a/lib/utils/color.dart b/lib/utils/color.dart index c562ae42..34c14585 100644 --- a/lib/utils/color.dart +++ b/lib/utils/color.dart @@ -30,5 +30,6 @@ class ColorUtils { Colors.cyan, Colors.blue, Colors.purple, + Colors.transparent, ]; } diff --git a/lib/utils/miru_storage.dart b/lib/utils/miru_storage.dart index ac014c1d..48b92200 100644 --- a/lib/utils/miru_storage.dart +++ b/lib/utils/miru_storage.dart @@ -129,6 +129,17 @@ class MiruStorage { await _initSetting(SettingKey.proxy, ''); 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); + await _initSetting( + SettingKey.ttsLanguage, Platform.localeName.split('_')[0]); + await _initSetting(SettingKey.ttsPitch, 1.0); + await _initSetting(SettingKey.ttsRate, 0.3); + await _initSetting(SettingKey.ttsVolume, 0.5); + await _initSetting(SettingKey.leading, 2.0); await _initSetting(SettingKey.subtitleFontSize, 46.0); await _initSetting(SettingKey.subtitleFontColor, Colors.white.value); await _initSetting(SettingKey.subtitleFontWeight, 'bold'); @@ -168,37 +179,49 @@ class MiruStorage { } class SettingKey { - 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 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 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.dart b/lib/views/pages/watch/reader/comic/comic_reader.dart index 008ad83b..9037efee 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader.dart @@ -50,27 +50,28 @@ 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), + 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()), ); } } 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..f18541ef 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_content.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_content.dart @@ -1,17 +1,19 @@ 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'; +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'; class ComicReaderContent extends StatefulWidget { const ComicReaderContent(this.tag, {super.key}); @@ -22,16 +24,13 @@ class ComicReaderContent extends StatefulWidget { } class _ComicReaderContentState extends State { - @override - void initState() { - super.initState(); - } - late final _c = Get.find(tag: widget.tag); // 按下数量 final List _pointer = []; - + final menuController = fluent.FlyoutController(); + final contextAttachKey = GlobalKey(); + 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; @@ -46,20 +45,20 @@ 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(), ), ), ), @@ -67,6 +66,217 @@ class _ComicReaderContentState extends State { ); } + Widget _indicatorBuilder() { + return Obx( + () => Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (_c.statusBarElement["reader-settings.page-indicator".i18n]! + .value) ...[ + Text( + "${_c.currentLocalProgress.value + 1}/${_c.itemlength[_c.index.value]}", + 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) + ], + ], + ), + ); + } + + 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; + 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) 之後,做出分割 + + 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( + scaleEnabled: _c.isZoom.value, + 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() + : 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, + // ), + // ) + // ] + [ + SliverList( + // itemExtent: height, + delegate: SliverChildBuilderDelegate( + (context, index) { + final url = listPrev[index]; + 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, + ), + ), + //設為中心點 + SliverList.builder( + // itemExtent: height, + key: _centerKey, + itemBuilder: (context, index) { + final img = _c.items[_c.positionedindex.value]; + final url = img[index]; + 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], + ), + SliverList( + // itemExtent: height, + delegate: SliverChildBuilderDelegate( + (context, index) { + final url = listNext[index]; + 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, + ), + ) + ], + ), + ), + ), + ); + }, + ); + } + _buildContent() { late Color backgroundColor; if (Platform.isAndroid) { @@ -74,16 +284,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; + return Obx(() { if (_c.error.value.isNotEmpty) { return Column( @@ -106,83 +317,67 @@ 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 SizedBox( - width: width, - height: height, - child: Listener( - onPointerDown: (event) { - _pointer.add(event.pointer); - if (_pointer.length == 2) { - _c.isZoom.value = true; + 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(); } - }, - onPointerUp: (event) { - _pointer.remove(event.pointer); - if (_pointer.length == 1) { - _c.isZoom.value = false; + } + 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(); } + } + // 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; }, - child: InteractiveViewer( - scaleEnabled: _c.isZoom.value, - child: ScrollablePositionedList.builder( - physics: _c.isZoom.value - ? const NeverScrollableScrollPhysics() - : null, + 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, ), - initialScrollIndex: cuurentPage, - itemScrollController: _c.itemScrollController, - itemPositionsListener: _c.itemPositionsListener, - scrollOffsetController: _c.scrollOffsetController, - itemBuilder: (context, index) { - final url = images[index]; - return CacheNetWorkImagePic( - url, - fit: BoxFit.fitWidth, - placeholder: _buildPlaceholder(context), - headers: _c.watchData.value?.headers, - ); - }, - itemCount: images.length, - ), - ), + child: imageBuilder(url), + ); + }, ), - ); - } - - //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: CacheNetWorkImagePic( - url, - mode: ExtendedImageMode.gesture, - key: ValueKey(url), - fit: BoxFit.contain, - placeholder: _buildPlaceholder(context), - headers: _c.watchData.value?.headers, - ), - ); - }, + ), ); }); }), @@ -191,15 +386,91 @@ class _ComicReaderContentState extends State { ); } + Widget imageBuilder(String url) { + return GestureDetector( + onTapDown: (deatils) { + _c.setControllPanel.value = !_c.setControllPanel.value; + }, + 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, + 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, context); + }, + ), + ], + ), + ), + ); + } + : null, + child: CacheNetWorkImagePic( + url, + + // postFrameCallback: (context) { + // RenderBox renderBox = + // context.currentContext!.findRenderObject() as RenderBox; + // logger.info('renderBox.size: ${renderBox.size}'); + // }, + fit: BoxFit.fitWidth, + placeholder: _buildPlaceholder(context), + headers: _c.watchData.value?.headers, + initGestureConfigHandler: (state) { + return GestureConfig( + inPageView: true, + ); + }, + )); + } + @override Widget build(BuildContext context) { + // _c.height.value = MediaQuery.of(context).size.height; return PlatformBuildWidget( androidBuilder: (context) { return Scaffold( - body: SafeArea( - child: _buildDisplay( - _buildContent(), - ), + body: _buildDisplay( + _buildContent(), )); }, desktopBuilder: (context) => _buildDisplay( 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..612994cb 100644 --- a/lib/views/pages/watch/reader/comic/comic_reader_settings.dart +++ b/lib/views/pages/watch/reader/comic/comic_reader_settings.dart @@ -1,10 +1,16 @@ 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'; 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'; +import 'package:window_manager/window_manager.dart'; class ComicReaderSettings extends StatefulWidget { const ComicReaderSettings(this.tag, {super.key}); @@ -16,104 +22,415 @@ class ComicReaderSettings extends StatefulWidget { class _ComicReaderSettingsState extends State { late final ComicController _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 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), - ), - ButtonSegment( - value: MangaReadMode.webTonn, - label: Text('comic-settings.web-tonn'.i18n), + 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: [ + // 阅读模式 + + 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: 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(), + ), + 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: () => _c.enableWakeLock.value, + onChanged: (val) { + WakelockPlus.toggle(enable: val); + _c.enableWakeLock.value = val; + MiruStorage.setSetting( + SettingKey.enableWakelock, val); + }), + ], + )), ), - ], - selected: {_c.readType.value}, - onSelectionChanged: (value) { - if (value.isNotEmpty) { - setState(() { - _c.readType.value = value.first; - }); - } - }, - showSelectedIcon: false, - ), - ), - ], - ), - ); + 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: "comic-settings.enable-autoscroller".i18n, + buildValue: () => _c.enableAutoScroll.value, + onChanged: (val) { + Get.back(); + _c.enableAutoScroll.value = val; + }), + const SizedBox(height: 16), + Text('comic-settings.autoscroller-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('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); + }, + ), + ], + )), + ) + ]), + ) + ], + )); } 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; - }); - } + return Obx( + () => fluent.Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: fluent.CommandBar( + isCompact: true, + 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: _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())); }, - child: Text('comic-settings.standard'.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.rightToLeft, - onChanged: (value) { - if (value) { - setState(() { - _c.readType.value = MangaReadMode.rightToLeft; - }); - } + builder: (context, mode, w) => Tooltip( + message: "comic-settings.page".i18n, + child: w, + )), + CommandBarText(text: "/ ${_c.watchData.value?.urls.length ?? 0}"), + const CommnadBarDivider(), + 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); + }, + checked: _c.enableWakeLock.value, + child: + const Icon(fluent.FluentIcons.coffee_script, size: 17)), + ), + const CommnadBarDivider(), + 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)), + ), + 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)) + ]), + ), + ), + ), + ), + ), + ); }, - child: Text('comic-settings.right-to-left'.i18n), ), - const SizedBox(width: 8), - fluent.ToggleButton( - checked: _c.readType.value == MangaReadMode.webTonn, - onChanged: (value) { - if (value) { - setState(() { - _c.readType.value = MangaReadMode.webTonn; - }); - } + ), + ), + 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)), + ), + ), + ), + ), + ), + ); }, - child: Text('comic-settings.web-tonn'.i18n), - ) - ], - ) + ), + ), + ), ], ), - ); - }); + ), + ); } @override 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..ef17d0fb 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,8 @@ 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'; +import 'package:bookfx/bookfx.dart'; class NovelReaderContent extends StatefulWidget { const NovelReaderContent(this.tag, {super.key}); @@ -18,198 +20,356 @@ class NovelReaderContent extends StatefulWidget { 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(); - }, - ) - ], + // 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; + } + 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(), ), - ); - } - - if (_c.watchData.value == null) { - return const Center(child: ProgressRing()); - } + )), + ], + ); + } - final watchData = _c.watchData.value!; + 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(); + }, + ) + ], + ), + ); + } - final listviewPadding = - maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; + if (_c.watchData.value == null) { + return const Center(child: ProgressRing()); + } + final listviewPadding = + maxWidth > 800 ? ((maxWidth - 800) / 2) : 16.0; - final fontSize = _c.fontSize.value; + 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 Center( - child: ScrollablePositionedList.builder( - itemPositionsListener: _c.itemPositionsListener, - initialScrollIndex: _c.positions.value, - 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 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), + ), + 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, + )), ); } - 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(); + + 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"); } - 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', - ), - ), - ], - ), - ), - ); + 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); }, - 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 _indicatorBuilder() { + return Obx(() => Row(mainAxisSize: MainAxisSize.min, children: [ + if (_c.statusBarElement["reader-settings.page-indicator".i18n]! + .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( + 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) + ], + ])); + } + + TextSpan _text(int index, double fontSize) { + final content = _c.items.expand((element) => element).toList(); + return TextSpan( + children: [ + const WidgetSpan(child: SizedBox(width: 40.0)), + TextSpan( + text: content[index], + style: TextStyle( + color: index == _c.currentLine.value + ? _c.highLightTextColor.value + : _c.textColor.value, + fontSize: fontSize, + fontWeight: FontWeight.w400, + backgroundColor: + index == _c.currentLine.value ? _c.highLightColor.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( + text: content[index], + style: TextStyle( + color: index == _c.currentLine.value + ? _c.highLightTextColor.value + : _c.textColor.value, + fontSize: fontSize, + fontWeight: FontWeight.w400, + backgroundColor: index == _c.currentLine.value + ? _c.highLightColor.value + : null, + height: leading, + 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/pages/watch/reader/novel/novel_reader_settings.dart b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart index 7dd0c4c9..8a84b8c8 100644 --- a/lib/views/pages/watch/reader/novel/novel_reader_settings.dart +++ b/lib/views/pages/watch/reader/novel/novel_reader_settings.dart @@ -2,8 +2,14 @@ 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'; +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}); @@ -15,53 +21,631 @@ 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 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, - ), - ), - ], - ), - ); + return DefaultTabController( + length: 3, + child: Column( + children: [ + TabBar(tabs: [ + Tab( + text: "common.common".i18n, + ), + 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: [ + // 阅读模式 + 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), + 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; + MiruStorage.setSetting( + SettingKey.novelFontSize, value); + }, + label: _c.fontSize.value.toString(), + 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; + MiruStorage.setSetting( + SettingKey.leading, value); + }, + label: _c.leading.value.toString(), + divisions: 40, + min: 0, + max: 4, + ), + ), + ], + )), + ), + 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)), + ), + 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.highLightColor.value = + ColorUtils.baseColors[index]; + } + }, + label: Container( + width: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorUtils.baseColors[index], + ), + ), + selected: ColorUtils.baseColors[index] == + _c.highLightColor.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.highLightTextColor.value = + ColorUtils.baseColors[index]; + } + }, + label: Container( + width: 20, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: ColorUtils.baseColors[index], + ), + ), + selected: ColorUtils.baseColors[index] == + _c.highLightTextColor.value)), + ), + ], + )), + ) + ]), + ) + ], + )); } 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.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, + ), + ), + ) ], ), ); diff --git a/lib/views/widgets/cache_network_image.dart b/lib/views/widgets/cache_network_image.dart index 3aa5a9b6..9a71fa32 100644 --- a/lib/views/widgets/cache_network_image.dart +++ b/lib/views/widgets/cache_network_image.dart @@ -24,6 +24,8 @@ class CacheNetWorkImagePic extends StatelessWidget { this.placeholder, this.canFullScreen = false, this.mode = ExtendedImageMode.none, + this.initGestureConfigHandler, + this.postFrameCallback, }); final String url; final BoxFit fit; @@ -34,7 +36,8 @@ class CacheNetWorkImagePic extends StatelessWidget { final bool canFullScreen; final Widget? placeholder; final ExtendedImageMode mode; - + final InitGestureConfigHandler? initGestureConfigHandler; + final void Function(GlobalKey contextkey)? postFrameCallback; _errorBuild() { if (fallback != null) { return fallback!; @@ -44,7 +47,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, @@ -52,6 +62,7 @@ class CacheNetWorkImagePic extends StatelessWidget { height: height, cache: true, mode: mode, + initGestureConfigHandler: initGestureConfigHandler, loadStateChanged: (state) { switch (state.extendedImageLoadState) { case LoadState.loading: @@ -91,6 +102,46 @@ class CacheNetWorkImagePic extends StatelessWidget { } } +void saveImage(url, Map? headers, 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 (context.mounted) { + final msg = result['isSuccess'] == true + ? 'common.save-success'.i18n + : result['errorMessage']; + showPlatformSnackbar( + context: context, + content: msg, + severity: fluent.InfoBarSeverity.success, + ); + } + 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,45 +164,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( @@ -193,6 +205,7 @@ class _ThumnailPageState extends State<_ThumnailPage> { appBar: AppBar(), body: GestureDetector( child: _buildContent(context), + onTapDown: (details) {}, onLongPress: () { showModalBottomSheet( context: context, @@ -207,7 +220,7 @@ class _ThumnailPageState extends State<_ThumnailPage> { title: Text('common.save'.i18n), onTap: () { Navigator.of(context).pop(); - _saveImage(); + saveImage(widget.url, widget.headers, context); }, ), ], @@ -238,7 +251,7 @@ class _ThumnailPageState extends State<_ThumnailPage> { text: Text('common.save'.i18n), onPressed: () { fluent.Flyout.of(context).close(); - _saveImage(); + saveImage(widget.url, widget.headers, context); }, ), ]); diff --git a/lib/views/widgets/watch/control_panel_footer.dart b/lib/views/widgets/watch/control_panel_footer.dart index de165cd6..8eaa0fcf 100644 --- a/lib/views/widgets/watch/control_panel_footer.dart +++ b/lib/views/widgets/watch/control_panel_footer.dart @@ -1,53 +1,150 @@ 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/utils/i18n.dart'; -import 'package:miru_app/views/widgets/button.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 + 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 - 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++; - }, + void initState() { + super.initState(); + ever(_c.watchData, (callback) { + progressObs.value = 0; + totalObs.value = _c.itemlength[_c.index.value]; + }); + ever(_c.index, (callback) { + progressObs.value = _c.progress.value; + totalObs.value = _c.itemlength[_c.index.value]; + }); + ever(_c.currentLocalProgress, (callback) { + progressObs.value = callback; + }); + } + + 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), ), ), - ).animate().fade(); + ); + } + + Widget _buildDesktop(BuildContext context) { + return const SizedBox.shrink(); + } + + @override + Widget build(BuildContext context) { + return PlatformBuildWidget( + androidBuilder: _buildAndroid, + desktopBuilder: _buildDesktop, + ); } } diff --git a/lib/views/widgets/watch/control_panel_header.dart b/lib/views/widgets/watch/control_panel_header.dart index 9c3a54fd..d090da88 100644 --- a/lib/views/widgets/watch/control_panel_header.dart +++ b/lib/views/widgets/watch/control_panel_header.dart @@ -27,120 +27,171 @@ 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( - child: Container( - height: 60, - color: Theme.of(context).scaffoldBackgroundColor, - child: AppBar( - title: Text(_c.title), - actions: [ - IconButton( - onPressed: () { - showModalBottomSheet( - context: context, - builder: (context) => 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(); - }, - ), - ); + 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: const Icon(Icons.list), - ), - ], + 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, + 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(); } Widget _buildDesktop(BuildContext context) { + final route = ModalRoute.of(context)?.settings.name; + debugPrint(route ?? ''); 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), + () => 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: Row( + mainAxisSize: MainAxisSize.min, + children: [ + fluent.IconButton( + icon: const Icon(fluent.FluentIcons.back), onPressed: () { - _settingFlayoutcontroller.showFlyout(builder: (context) { - return widget.buildSettings(context); - }); + RouterUtils.pop(); }, ), - ), - 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(); - }, - ), - ), - ); - }); - }, + 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, + ), + ) + ], + ), ), ), - ).animate().fade(), + fluent.Container( + height: 60, + color: fluent.FluentTheme.of(context).micaBackgroundColor, + child: widget.buildSettings(context), + ), + // Obx()) + ]).animate().fade(), + ); + } + + Widget commandBaruilder(child) { + return Padding( + padding: const EdgeInsets.all(4), + child: child, ); } @@ -151,4 +202,10 @@ class _ControlPanelHeaderState desktopBuilder: _buildDesktop, ); } + + @override + void dispose() { + _playListFlayoutcontroller.dispose(); + super.dispose(); + } } 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..13c83927 --- /dev/null +++ b/lib/views/widgets/watch/desktop_command_bar.dart @@ -0,0 +1,171 @@ +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, + )) + ]); + } +} diff --git a/lib/views/widgets/watch/playlist.dart b/lib/views/widgets/watch/playlist.dart index 1557a366..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); }, ); }, @@ -38,20 +60,23 @@ class PlayList extends fluent.StatelessWidget { } Widget _buildDesktop(BuildContext context) { - return ScrollablePositionedList.builder( - itemCount: list.length, - initialScrollIndex: selectIndex, - padding: const EdgeInsets.all(8), - itemBuilder: (context, index) { - final contact = list[index]; - return fluent.ListTile.selectable( - title: Text(contact), - onPressed: () { - onChange(index); - }, - selected: list[selectIndex] == contact, - ); - }, + return Container( + padding: const EdgeInsets.all(16), + color: fluent.FluentTheme.of(context).micaBackgroundColor, + child: ScrollablePositionedList.builder( + itemCount: list.length, + initialScrollIndex: selectIndex, + itemBuilder: (context, index) { + final contact = list[index]; + return fluent.ListTile.selectable( + title: Text(contact), + selected: list[selectIndex] == contact, + onSelectionChange: (value) { + widget.onChange(index); + }, + ); + }, + ), ); } diff --git a/lib/views/widgets/watch/reader_view.dart b/lib/views/widgets/watch/reader_view.dart index 5bc02245..1c2d7887 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'; @@ -19,46 +21,73 @@ class ReaderView extends StatelessWidget { @override Widget build(BuildContext context) { final c = Get.find(tag: tag); + final width = LayoutUtils.width; return Obx( () => Stack( children: [ MouseRegion( - onHover: (event) { - if (event.position.dy < 60) { - c.showControlPanel(); - } - if (event.position.dy > LayoutUtils.height - 60) { - c.showControlPanel(); - } - }, + 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, ), // 点击中间显示控制面板 // 左边上一页右边下一页 - 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 || !c.enableTapRegion.value) ...[ + 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) ...[ + , + if (c.isShowControlPanel.value || c.enableAutoScroll.value) ...[ // 顶部控制 Positioned( child: ControlPanelHeader( @@ -67,13 +96,8 @@ class ReaderView extends StatelessWidget { ), ), // 底部控制 - Positioned( - right: 0, - left: 0, - bottom: 0, - child: ControlPanelFooter(tag), - ), - ] + ], + ControlPanelFooter(tag), ], ), ); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 882d01b7..0093404b 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,10 +5,12 @@ import FlutterMacOS import Foundation +import battery_plus 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 @@ -22,10 +24,12 @@ 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")) 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 5cc0d294..13287b01 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,38 @@ 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" + bookfx: + dependency: "direct main" + description: + name: bookfx + sha256: "115e64b263d077943440c885acf4a249bcdb1ba5a2ad26c41b9c027292fa0010" + url: "https://pub.dev" + source: hosted + version: "0.0.2" boolean_selector: dependency: transitive description: @@ -544,6 +576,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 @@ -686,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: @@ -1363,6 +1411,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: @@ -1460,7 +1516,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 868f0e6b..d1879fa2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -59,11 +59,16 @@ 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 + flutter_tts: ^3.8.5 + bookfx: ^0.0.2 volume_controller: ^2.0.7 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 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 2a40c6ec..761360b5 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,8 +6,10 @@ #include "generated_plugin_registrant.h" +#include #include #include +#include #include #include #include @@ -19,10 +21,14 @@ #include void RegisterPlugins(flutter::PluginRegistry* registry) { + BatteryPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); DesktopMultiWindowPluginRegisterWithRegistrar( 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 12c4f110..7d21ff90 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,8 +3,10 @@ # 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