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