From ce0d10aeb22abbdd85d0f01c432dd3c2d2891209 Mon Sep 17 00:00:00 2001 From: enximi <70036307+enximi@users.noreply.github.com> Date: Sun, 10 Aug 2025 16:02:44 +0800 Subject: [PATCH] Add a feature to allow saving custom reader settings for each comic. (#459) * Add a feature to allow saving custom reader settings for each comic. * Comic-specific settings disabled by default --- assets/translation.json | 10 +- lib/foundation/appdata.dart | 81 ++++-- lib/pages/reader/gesture.dart | 21 +- lib/pages/reader/images.dart | 154 ++++++----- lib/pages/reader/reader.dart | 99 +++---- lib/pages/reader/scaffold.dart | 250 ++++++++++------- lib/pages/settings/reader.dart | 89 +++++- lib/pages/settings/setting_components.dart | 308 ++++++++++++++------- 8 files changed, 658 insertions(+), 354 deletions(-) diff --git a/assets/translation.json b/assets/translation.json index ae4c74b..66f3d85 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -406,7 +406,10 @@ "Disable Length Limitation": "禁用长度限制", "Only valid for this run": "仅对本次运行有效", "Logs": "日志", - "Export logs": "导出日志" + "Export logs": "导出日志", + "Clear specific reader settings for all comics": "清除所有漫画的特殊阅读设置", + "Clear specific reader settings for this comic": "清除该漫画的特殊阅读设置", + "Enable comic specific settings": "为每本漫画保存特定设置" }, "zh_TW": { "Home": "首頁", @@ -815,6 +818,9 @@ "Disable Length Limitation": "禁用長度限制", "Only valid for this run": "僅對本次運行有效", "Logs": "日誌", - "Export logs": "匯出日誌" + "Export logs": "匯出日誌", + "Clear specific reader settings for all comics": "清除所有漫畫的特殊閱讀設定", + "Clear specific reader settings for this comic": "清除該漫畫的特殊閱讀設定", + "Enable comic specific settings": "為每本漫畫保存特定設定" } } \ No newline at end of file diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index 7c691f9..6998a25 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -26,8 +26,7 @@ class Appdata with Init { var data = jsonEncode(toJson()); var file = File(FilePath.join(App.dataPath, 'appdata.json')); await file.writeAsString(data); - } - finally { + } finally { _isSavingData = false; } if (sync) { @@ -57,10 +56,7 @@ class Appdata with Init { } Map toJson() { - return { - 'settings': settings._data, - 'searchHistory': searchHistory, - }; + return {'settings': settings._data, 'searchHistory': searchHistory}; } /// Following fields are related to device-specific data and should not be synced. @@ -95,8 +91,7 @@ class Appdata with Init { try { var file = File(FilePath.join(App.dataPath, 'implicitData.json')); await file.writeAsString(jsonEncode(implicitData)); - } - finally { + } finally { _isSavingData = false; } } @@ -104,10 +99,7 @@ class Appdata with Init { @override Future doInit() async { var dataPath = (await getApplicationSupportDirectory()).path; - var file = File(FilePath.join( - dataPath, - 'appdata.json', - )); + var file = File(FilePath.join(dataPath, 'appdata.json')); if (!await file.exists()) { return; } @@ -119,8 +111,7 @@ class Appdata with Init { } } searchHistory = List.from(json['searchHistory']); - } - catch(e) { + } catch (e) { Log.error("Appdata", "Failed to load appdata", e); Log.info("Appdata", "Resetting appdata"); file.deleteIgnoreError(); @@ -130,8 +121,7 @@ class Appdata with Init { if (await implicitDataFile.exists()) { implicitData = jsonDecode(await implicitDataFile.readAsString()); } - } - catch (e) { + } catch (e) { Log.error("Appdata", "Failed to load implicit data", e); Log.info("Appdata", "Resetting implicit data"); var implicitDataFile = File(FilePath.join(dataPath, 'implicitData.json')); @@ -162,6 +152,7 @@ class Settings with ChangeNotifier { 'blockedWords': [], 'defaultSearchTarget': null, 'autoPageTurningInterval': 5, // in seconds + 'enableComicSpecificSettings': false, 'readerMode': 'galleryLeftToRight', // values of [ReaderMode] 'readerScreenPicNumberForLandscape': 1, // 1 - 5 'readerScreenPicNumberForPortrait': 1, // 1 - 5 @@ -199,6 +190,7 @@ class Settings with ChangeNotifier { 'enableDoubleTapToZoom': true, 'reverseChapterOrder': false, 'showSystemStatusBar': false, + 'comicSpecificSettings': >{}, }; operator [](String key) { @@ -212,6 +204,60 @@ class Settings with ChangeNotifier { } } + bool haveComicSpecificSettings(String comicId, String sourceKey, String key) { + return _data['comicSpecificSettings']?["$comicId@$sourceKey"]?.containsKey( + key, + ) ?? + false; + } + + dynamic getReaderSetting(String comicId, String sourceKey, String key) { + if (key == 'enableComicSpecificSettings') { + return _data['enableComicSpecificSettings']; + } + if (_data['enableComicSpecificSettings'] == false) { + return _data[key]; + } + return _data['comicSpecificSettings']["$comicId@$sourceKey"]?[key] ?? + _data[key]; + } + + void setReaderSetting( + String comicId, + String sourceKey, + String key, + dynamic value, + ) { + if (key == 'enableComicSpecificSettings') { + _data['enableComicSpecificSettings'] = value; + notifyListeners(); + return; + } + if (_data['enableComicSpecificSettings'] == false) { + _data[key] = value; + notifyListeners(); + return; + } + (_data['comicSpecificSettings'] as Map).putIfAbsent( + "$comicId@$sourceKey", + () => {}, + )[key] = value; + notifyListeners(); + } + + void resetComicReaderSettings(String comicId, String sourceKey) { + final allComicSettings = _data['comicSpecificSettings'] as Map; + if (allComicSettings.containsKey("$comicId@$sourceKey")) { + allComicSettings.remove("$comicId@$sourceKey"); + } + notifyListeners(); + } + + void resetAllComicReaderSettings() { + _data['comicSpecificSettings'] = >{}; + notifyListeners(); + } + @override String toString() { return _data.toString(); @@ -236,4 +282,5 @@ function processImage(image, cid, eid, page, sourceKey) { } '''; -const _defaultSourceListUrl = "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/index.json"; +const _defaultSourceListUrl = + "https://git.nyne.dev/nyne/venera-configs/raw/branch/main/index.json"; diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart index 04a68e9..59e840f 100644 --- a/lib/pages/reader/gesture.dart +++ b/lib/pages/reader/gesture.dart @@ -131,11 +131,11 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet } if (context.reader.mode.key.startsWith('gallery')) { if (forward) { - if (!context.reader.toNextPage() && !context.reader.isLastChapterOfGroup) { + if (!context.reader.toNextPage(reader.cid, reader.type) && !context.reader.isLastChapterOfGroup) { context.reader.toNextChapter(); } } else { - if (!context.reader.toPrevPage() && !context.reader.isFirstChapterOfGroup) { + if (!context.reader.toPrevPage(reader.cid, reader.type) && !context.reader.isFirstChapterOfGroup) { context.reader.toPrevChapter(); } } @@ -152,7 +152,8 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet bool _dragInProgress = false; - bool get _enableDoubleTapToZoom => appdata.settings["enableDoubleTapToZoom"]; + bool get _enableDoubleTapToZoom => + appdata.settings.getReaderSetting(reader.cid, reader.type.sourceKey, 'enableDoubleTapToZoom'); void onTapUp(TapUpDetails event) { if (_longPressInProgress) { @@ -190,7 +191,8 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet } else if (context.readerScaffold.isOpen) { context.readerScaffold.openOrClose(); } else { - if (appdata.settings['enableTapToTurnPages']) { + if (appdata.settings.getReaderSetting( + reader.cid, reader.type.sourceKey, 'enableTapToTurnPages')) { bool isLeft = false, isRight = false, isTop = false, isBottom = false; final width = context.width; final height = context.height; @@ -207,11 +209,12 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet isBottom = true; } bool isCenter = false; - var prev = context.reader.toPrevPage; - var next = context.reader.toNextPage; - if (appdata.settings['reverseTapToTurnPages']) { - prev = context.reader.toNextPage; - next = context.reader.toPrevPage; + var prev = () => context.reader.toPrevPage(context.reader.cid, context.reader.type); + var next = () => context.reader.toNextPage(context.reader.cid, context.reader.type); + if (appdata.settings.getReaderSetting( + reader.cid, reader.type.sourceKey, 'reverseTapToTurnPages')) { + prev = () => context.reader.toNextPage(context.reader.cid, context.reader.type); + next = () => context.reader.toPrevPage(context.reader.cid, context.reader.type); } switch (context.reader.mode) { case ReaderMode.galleryLeftToRight: diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 0b3ebc0..917f20f 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -32,10 +32,17 @@ class _ReaderImagesState extends State<_ReaderImages> { inProgress = true; if (reader.type == ComicType.local || (LocalManager().isDownloaded( - reader.cid, reader.type, reader.chapter, reader.widget.chapters))) { + reader.cid, + reader.type, + reader.chapter, + reader.widget.chapters, + ))) { try { - var images = await LocalManager() - .getImages(reader.cid, reader.type, reader.chapter); + var images = await LocalManager().getImages( + reader.cid, + reader.type, + reader.chapter, + ); setState(() { reader.images = images; reader.isLoading = false; @@ -81,9 +88,7 @@ class _ReaderImagesState extends State<_ReaderImages> { Widget build(BuildContext context) { if (reader.isLoading) { load(); - return const Center( - child: CircularProgressIndicator(), - ); + return const Center(child: CircularProgressIndicator()); } else if (error != null) { return GestureDetector( onTap: () { @@ -104,7 +109,8 @@ class _ReaderImagesState extends State<_ReaderImages> { } else { if (reader.mode.isGallery) { return _GalleryMode( - key: Key('${reader.mode.key}_${reader.imagesPerPage}')); + key: Key('${reader.mode.key}_${reader.imagesPerPage}'), + ); } else { return _ContinuousMode(key: Key(reader.mode.key)); } @@ -132,11 +138,15 @@ class _GalleryModeState extends State<_GalleryMode> /// [totalPages] is the total number of pages in the current chapter. /// More than one images can be displayed on one page. int get totalPages { - if (!reader.showSingleImageOnFirstPage) { - return (reader.images!.length / reader.imagesPerPage).ceil(); + if (!reader.showSingleImageOnFirstPage(reader.cid, reader.type)) { + return (reader.images!.length / + reader.imagesPerPage(reader.cid, reader.type)) + .ceil(); } else { return 1 + - ((reader.images!.length - 1) / reader.imagesPerPage).ceil(); + ((reader.images!.length - 1) / + reader.imagesPerPage(reader.cid, reader.type)) + .ceil(); } } @@ -159,19 +169,24 @@ class _GalleryModeState extends State<_GalleryMode> /// Get the range of images for the given page. [page] is 1-based. (int start, int end) getPageImagesRange(int page) { - if (reader.showSingleImageOnFirstPage) { + var imagesPerPage = reader.imagesPerPage(reader.cid, reader.type); + if (reader.showSingleImageOnFirstPage(reader.cid, reader.type)) { if (page == 1) { return (0, 1); } else { - int startIndex = (page - 2) * reader.imagesPerPage + 1; + int startIndex = (page - 2) * imagesPerPage + 1; int endIndex = math.min( - startIndex + reader.imagesPerPage, reader.images!.length); + startIndex + imagesPerPage, + reader.images!.length, + ); return (startIndex, endIndex); } } else { - int startIndex = (page - 1) * reader.imagesPerPage; + int startIndex = (page - 1) * imagesPerPage; int endIndex = math.min( - startIndex + reader.imagesPerPage, reader.images!.length); + startIndex + imagesPerPage, + reader.images!.length, + ); return (startIndex, endIndex); } } @@ -193,9 +208,9 @@ class _GalleryModeState extends State<_GalleryMode> var (startIndex, endIndex) = getPageImagesRange(page); for (int i = startIndex; i < endIndex; i++) { if (shouldPreCache) { - _precacheImage(i+1, context); + _precacheImage(i + 1, context); } else { - _preDownloadImage(i+1, context); + _preDownloadImage(i + 1, context); } } } @@ -217,16 +232,12 @@ class _GalleryModeState extends State<_GalleryMode> var controller = photoViewControllers[reader.page]!; Offset value = event.delta; if (isLongPressing) { - controller.updateMultiple( - position: controller.position + value, - ); + controller.updateMultiple(position: controller.position + value); } } }, child: PhotoViewGallery.builder( - backgroundDecoration: BoxDecoration( - color: context.colorScheme.surface, - ), + backgroundDecoration: BoxDecoration(color: context.colorScheme.surface), reverse: reader.mode == ReaderMode.galleryRightToLeft, scrollDirection: reader.mode == ReaderMode.galleryTopToBottom ? Axis.vertical @@ -239,14 +250,17 @@ class _GalleryModeState extends State<_GalleryMode> ); } else { var (startIndex, endIndex) = getPageImagesRange(index); - List pageImages = - reader.images!.sublist(startIndex, endIndex); + List pageImages = reader.images!.sublist( + startIndex, + endIndex, + ); cache(index); photoViewControllers[index] ??= PhotoViewController(); - if (reader.imagesPerPage == 1 || pageImages.length == 1) { + if (reader.imagesPerPage(reader.cid, reader.type) == 1 || + pageImages.length == 1) { return PhotoViewGalleryPageOptions( filterQuality: FilterQuality.medium, controller: photoViewControllers[index], @@ -287,11 +301,11 @@ class _GalleryModeState extends State<_GalleryMode> onPageChanged: (i) { if (i == 0) { if (reader.isFirstChapterOfGroup || !reader.toPrevChapter()) { - reader.toPage(1); + reader.toPage(reader.cid, reader.type, 1); } } else if (i == totalPages + 1) { if (reader.isLastChapterOfGroup || !reader.toNextChapter()) { - reader.toPage(totalPages); + reader.toPage(reader.cid, reader.type, totalPages); } } else { reader.setPage(i); @@ -356,13 +370,16 @@ class _GalleryModeState extends State<_GalleryMode> onInit: (state) => imageStates.add(state), onDispose: (state) => imageStates.remove(state), ), - ) + ), ]; } else { imageWidgets = images.map((imageKey) { startIndex++; - ImageProvider imageProvider = - _createImageProviderFromKey(imageKey, context, startIndex); + ImageProvider imageProvider = _createImageProviderFromKey( + imageKey, + context, + startIndex, + ); return Expanded( child: ComicImage( image: imageProvider, @@ -423,10 +440,7 @@ class _GalleryModeState extends State<_GalleryMode> } else { zoomPosition = Offset(0, 0); } - photoViewController.animateScale?.call( - target, - zoomPosition, - ); + photoViewController.animateScale?.call(target, zoomPosition); isLongPressing = true; } @@ -471,14 +485,14 @@ class _GalleryModeState extends State<_GalleryMode> keyRepeatTimer = null; } if (forward == true) { - reader.toPage(reader.page+1); + reader.toPage(reader.cid, reader.type, reader.page + 1); } else if (forward == false) { - reader.toPage(reader.page-1); + reader.toPage(reader.cid, reader.type, reader.page - 1); } } if (event is KeyRepeatEvent && keyRepeatTimer == null) { keyRepeatTimer = Timer.periodic( - reader.enablePageAnimation + reader.enablePageAnimation(reader.cid, reader.type) ? const Duration(milliseconds: 200) : const Duration(milliseconds: 50), (timer) { @@ -486,9 +500,9 @@ class _GalleryModeState extends State<_GalleryMode> timer.cancel(); return; } else if (forward == true) { - reader.toPage(reader.page+1); + reader.toPage(reader.cid, reader.type, reader.page + 1); } else if (forward == false) { - reader.toPage(reader.page-1); + reader.toPage(reader.cid, reader.type, reader.page - 1); } }, ); @@ -512,15 +526,15 @@ class _GalleryModeState extends State<_GalleryMode> return await File(imageKey.substring(7)).readAsBytes(); } else { return (await CacheManager().findCache( - "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! - .readAsBytes(); + "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}", + ))!.readAsBytes(); } } @override String? getImageKeyByOffset(Offset offset) { String? imageKey; - if (reader.imagesPerPage == 1) { + if (reader.imagesPerPage(reader.cid, reader.type) == 1) { imageKey = reader.images![reader.page - 1]; } else { for (var imageState in imageStates) { @@ -538,7 +552,7 @@ const Set _kTouchLikeDeviceTypes = { PointerDeviceKind.mouse, PointerDeviceKind.stylus, PointerDeviceKind.invertedStylus, - PointerDeviceKind.unknown + PointerDeviceKind.unknown, }; const double _kChangeChapterOffset = 160; @@ -673,10 +687,12 @@ class _ContinuousModeState extends State<_ContinuousMode> void onScroll() { if (prepareToPrevChapter) { jumpToNextChapter = false; - jumpToPrevChapter = scrollController.offset < + jumpToPrevChapter = + scrollController.offset < scrollController.position.minScrollExtent - _kChangeChapterOffset; } else if (prepareToNextChapter) { - jumpToNextChapter = scrollController.offset > + jumpToNextChapter = + scrollController.offset > scrollController.position.maxScrollExtent + _kChangeChapterOffset; jumpToPrevChapter = false; } @@ -721,8 +737,8 @@ class _ContinuousModeState extends State<_ContinuousMode> physics: isCTRLPressed || _isMouseScrolling || disableScroll ? const NeverScrollableScrollPhysics() : isZoomedIn - ? const ClampingScrollPhysics() - : const BouncingScrollPhysics(), + ? const ClampingScrollPhysics() + : const BouncingScrollPhysics(), itemBuilder: (context, index) { if (index == 0 || index == reader.maxPage + 1) { return const SizedBox(); @@ -750,8 +766,10 @@ class _ContinuousModeState extends State<_ContinuousMode> ), ); }, - scrollBehavior: const MaterialScrollBehavior() - .copyWith(scrollbars: false, dragDevices: _kTouchLikeDeviceTypes), + scrollBehavior: const MaterialScrollBehavior().copyWith( + scrollbars: false, + dragDevices: _kTouchLikeDeviceTypes, + ), ); widget = Stack( @@ -895,20 +913,14 @@ class _ContinuousModeState extends State<_ContinuousMode> } return PhotoView.customChild( - backgroundDecoration: BoxDecoration( - color: context.colorScheme.surface, - ), + backgroundDecoration: BoxDecoration(color: context.colorScheme.surface), childSize: Size(width, height), minScale: 1.0, maxScale: 2.5, strictScale: true, controller: photoViewController, onScaleUpdate: onScaleUpdate, - child: SizedBox( - width: width, - height: height, - child: widget, - ), + child: SizedBox(width: width, height: height, child: widget), ); } @@ -978,10 +990,7 @@ class _ContinuousModeState extends State<_ContinuousMode> } else { zoomPosition = Offset(0, 0); } - photoViewController.animateScale?.call( - target, - zoomPosition, - ); + photoViewController.animateScale?.call(target, zoomPosition); onScaleUpdate(target); isLongPressing = true; } @@ -1069,8 +1078,8 @@ class _ContinuousModeState extends State<_ContinuousMode> return await File(imageKey.substring(7)).readAsBytes(); } else { return (await CacheManager().findCache( - "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! - .readAsBytes(); + "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}", + ))!.readAsBytes(); } } @@ -1114,10 +1123,7 @@ void _precacheImage(int page, BuildContext context) { if (page <= 0 || page > context.reader.images!.length) { return; } - precacheImage( - _createImageProvider(page, context), - context, - ); + precacheImage(_createImageProvider(page, context), context); } /// [_preDownloadImage] is used to download the image for the given page. @@ -1138,10 +1144,7 @@ void _preDownloadImage(int page, BuildContext context) { } class _SwipeChangeChapterProgress extends StatefulWidget { - const _SwipeChangeChapterProgress({ - this.controller, - required this.isPrev, - }); + const _SwipeChangeChapterProgress({this.controller, required this.isPrev}); final ScrollController? controller; @@ -1258,7 +1261,12 @@ class _ProgressPainter extends CustomPainter { paint.color = color; canvas.drawRRect( RRect.fromLTRBR( - 0, 0, size.width * value, size.height, Radius.circular(16)), + 0, + 0, + size.width * value, + size.height, + Radius.circular(16), + ), paint, ); } diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index c0148f3..6b6f9d8 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -115,10 +115,10 @@ class _ReaderState extends State if (images == null) { return 1; } - if (!showSingleImageOnFirstPage) { - return (images!.length / imagesPerPage).ceil(); + if (!showSingleImageOnFirstPage(cid, type)) { + return (images!.length / imagesPerPage(cid, type)).ceil(); } else { - return 1 + ((images!.length - 1) / imagesPerPage).ceil(); + return 1 + ((images!.length - 1) / imagesPerPage(cid, type)).ceil(); } } @@ -162,13 +162,14 @@ class _ReaderState extends State if (widget.initialPage != null) { page = widget.initialPage!; } - mode = ReaderMode.fromKey(appdata.settings['readerMode']); + // mode = ReaderMode.fromKey(appdata.settings['readerMode']); + mode = ReaderMode.fromKey(appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerMode')); history = widget.history; - if (!appdata.settings['showSystemStatusBar']) { + if (!appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSystemStatusBar')) { SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); } - if (appdata.settings['enableTurnPageByVolumeKey']) { - handleVolumeEvent(); + if (appdata.settings.getReaderSetting(cid, type.sourceKey, 'enableTurnPageByVolumeKey')) { + handleVolumeEvent(cid, type); } setImageCacheSize(); Future.delayed(const Duration(milliseconds: 200), () { @@ -183,11 +184,11 @@ class _ReaderState extends State void didChangeDependencies() { super.didChangeDependencies(); if (!_isInitialized) { - initImagesPerPage(widget.initialPage ?? 1); + initImagesPerPage(cid, type, widget.initialPage ?? 1); _isInitialized = true; } else { // For orientation changed - _checkImagesPerPageChange(); + _checkImagesPerPageChange(cid, type); } initReaderWindow(); } @@ -229,7 +230,7 @@ class _ReaderState extends State @override Widget build(BuildContext context) { - _checkImagesPerPageChange(); + _checkImagesPerPageChange(cid, type); return KeyboardListener( focusNode: focusNode, autofocus: true, @@ -274,13 +275,13 @@ class _ReaderState extends State history!.page = images?.length ?? 1; } else { /// Record the first image of the page - if (!showSingleImageOnFirstPage || imagesPerPage == 1) { - history!.page = (page - 1) * imagesPerPage + 1; + if (!showSingleImageOnFirstPage(cid, type) || imagesPerPage(cid, type) == 1) { + history!.page = (page - 1) * imagesPerPage(cid, type) + 1; } else { if (page == 1) { history!.page = 1; } else { - history!.page = (page - 2) * imagesPerPage + 2; + history!.page = (page - 2) * imagesPerPage(cid, type) + 2; } } } @@ -363,39 +364,39 @@ abstract mixin class _ImagePerPageHandler { ReaderMode get mode; - void initImagesPerPage(int initialPage) { - _lastImagesPerPage = imagesPerPage; + void initImagesPerPage(String cid, ComicType type, int initialPage) { + _lastImagesPerPage = imagesPerPage(cid, type); _lastOrientation = isPortrait; - if (imagesPerPage != 1) { - if (showSingleImageOnFirstPage) { - page = ((initialPage - 1) / imagesPerPage).ceil() + 1; + if (imagesPerPage(cid, type) != 1) { + if (showSingleImageOnFirstPage(cid, type)) { + page = ((initialPage - 1) / imagesPerPage(cid, type)).ceil() + 1; } else { - page = (initialPage / imagesPerPage).ceil(); + page = (initialPage / imagesPerPage(cid, type)).ceil(); } } } - bool get showSingleImageOnFirstPage => - appdata.settings["showSingleImageOnFirstPage"]; + bool showSingleImageOnFirstPage(String cid, ComicType type) => + appdata.settings.getReaderSetting(cid, type.sourceKey, 'showSingleImageOnFirstPage'); /// The number of images displayed on one screen - int get imagesPerPage { + int imagesPerPage(String cid, ComicType type) { if (mode.isContinuous) return 1; if (isPortrait) { - return appdata.settings['readerScreenPicNumberForPortrait'] ?? 1; + return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForPortrait') ?? 1; } else { - return appdata.settings['readerScreenPicNumberForLandscape'] ?? 1; + return appdata.settings.getReaderSetting(cid, type.sourceKey, 'readerScreenPicNumberForLandscape') ?? 1; } } /// Check if the number of images per page has changed - void _checkImagesPerPageChange() { - int currentImagesPerPage = imagesPerPage; + void _checkImagesPerPageChange(String cid, ComicType type) { + int currentImagesPerPage = imagesPerPage(cid, type); bool currentOrientation = isPortrait; if (_lastImagesPerPage != currentImagesPerPage || _lastOrientation != currentOrientation) { _adjustPageForImagesPerPageChange( - _lastImagesPerPage, currentImagesPerPage); + cid, type, _lastImagesPerPage, currentImagesPerPage); _lastImagesPerPage = currentImagesPerPage; _lastOrientation = currentOrientation; } @@ -403,9 +404,9 @@ abstract mixin class _ImagePerPageHandler { /// Adjust the page number when the number of images per page changes void _adjustPageForImagesPerPageChange( - int oldImagesPerPage, int newImagesPerPage) { + String cid, ComicType type, int oldImagesPerPage, int newImagesPerPage) { int previousImageIndex = 1; - if (!showSingleImageOnFirstPage || oldImagesPerPage == 1) { + if (!showSingleImageOnFirstPage(cid, type) || oldImagesPerPage == 1) { previousImageIndex = (page - 1) * oldImagesPerPage + 1; } else { if (page == 1) { @@ -417,7 +418,7 @@ abstract mixin class _ImagePerPageHandler { int newPage; if (newImagesPerPage != 1) { - if (showSingleImageOnFirstPage) { + if (showSingleImageOnFirstPage(cid, type)) { newPage = ((previousImageIndex - 1) / newImagesPerPage).ceil() + 1; } else { newPage = (previousImageIndex / newImagesPerPage).ceil(); @@ -431,9 +432,9 @@ abstract mixin class _ImagePerPageHandler { } abstract mixin class _VolumeListener { - bool toNextPage(); + bool toNextPage(String cid, ComicType type); - bool toPrevPage(); + bool toPrevPage(String cid, ComicType type); bool toNextChapter(); @@ -441,19 +442,19 @@ abstract mixin class _VolumeListener { VolumeListener? volumeListener; - void onDown() { - if (!toNextPage()) { + void onDown(String cid, ComicType type) { + if (!toNextPage(cid, type)) { toNextChapter(); } } - void onUp() { - if (!toPrevPage()) { + void onUp(String cid, ComicType type) { + if (!toPrevPage(cid, type)) { toPrevChapter(); } } - void handleVolumeEvent() { + void handleVolumeEvent(String cid, ComicType type) { if (!App.isAndroid) { // Currently only support Android return; @@ -462,8 +463,8 @@ abstract mixin class _VolumeListener { volumeListener?.cancel(); } volumeListener = VolumeListener( - onDown: onDown, - onUp: onUp, + onDown: () => onDown(cid, type), + onUp: () => onUp(cid, type), )..listen(); } @@ -495,7 +496,7 @@ abstract mixin class _ReaderLocation { void update(); - bool get enablePageAnimation => appdata.settings['enablePageAnimation']; + bool enablePageAnimation(String cid, ComicType type) => appdata.settings.getReaderSetting(cid, type.sourceKey, 'enablePageAnimation'); _ImageViewController? _imageViewController; @@ -514,25 +515,25 @@ abstract mixin class _ReaderLocation { } /// Returns true if the page is changed - bool toNextPage() { - return toPage(page + 1); + bool toNextPage(String cid, ComicType type) { + return toPage(cid, type, page + 1); } /// Returns true if the page is changed - bool toPrevPage() { - return toPage(page - 1); + bool toPrevPage(String cid, ComicType type) { + return toPage(cid, type, page - 1); } int _animationCount = 0; - bool toPage(int page) { + bool toPage(String cid, ComicType type, int page) { if (_validatePage(page)) { if (page == this.page && page != 1 && page != maxPage) { return false; } this.page = page; update(); - if (enablePageAnimation) { + if (enablePageAnimation(cid, type)) { _animationCount++; _imageViewController!.animateToPage(page).then((_) { _animationCount--; @@ -571,17 +572,17 @@ abstract mixin class _ReaderLocation { Timer? autoPageTurningTimer; - void autoPageTurning() { + void autoPageTurning(String cid, ComicType type) { if (autoPageTurningTimer != null) { autoPageTurningTimer!.cancel(); autoPageTurningTimer = null; } else { - int interval = appdata.settings['autoPageTurningInterval']; + int interval = appdata.settings.getReaderSetting(cid, type.sourceKey, 'autoPageTurningInterval'); autoPageTurningTimer = Timer.periodic(Duration(seconds: interval), (_) { if (page == maxPage) { autoPageTurningTimer!.cancel(); } - toNextPage(); + toNextPage(cid, type); }); } } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index aa23430..ad93fdc 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -128,9 +128,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { Widget build(BuildContext context) { return Stack( children: [ - Positioned.fill( - child: widget.child, - ), + Positioned.fill(child: widget.child), if (appdata.settings['showPageNumberInReader'] == true) buildPageInfoText(), buildStatusInfo(), @@ -168,10 +166,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { decoration: BoxDecoration( color: context.colorScheme.surface.toOpacity(0.92), border: Border( - bottom: BorderSide( - color: Colors.grey.toOpacity(0.5), - width: 0.5, - ), + bottom: BorderSide(color: Colors.grey.toOpacity(0.5), width: 0.5), ), ), child: Row( @@ -216,8 +211,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { try { if (context.reader.images![0].contains('file://')) { showToast( - message: "Local comic collection is not supported at present".tl, - context: context); + message: "Local comic collection is not supported at present".tl, + context: context, + ); return; } String id = context.reader.cid; @@ -234,8 +230,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { List tags = context.reader.widget.tags; String author = context.reader.widget.author; - var epName = context.reader.widget.chapters?.titles - .elementAtOrNull(context.reader.chapter - 1) ?? + var epName = + context.reader.widget.chapters?.titles.elementAtOrNull( + context.reader.chapter - 1, + ) ?? "E${context.reader.chapter}"; var translatedTags = tags.map((e) => e.translateTagsToCN).toList(); @@ -248,7 +246,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { return; } ImageFavoriteManager().deleteImageFavorite([ - ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName) + ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName), ]); showToast( message: "Uncollected the image".tl, @@ -256,7 +254,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { seconds: 1, ); } else { - var imageFavoritesComic = ImageFavoriteManager().find(id, sourceKey) ?? + var imageFavoritesComic = + ImageFavoriteManager().find(id, sourceKey) ?? ImageFavoritesComic( id, [], @@ -270,12 +269,21 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { subTitle, maxPage, ); - ImageFavorite imageFavorite = - ImageFavorite(page, imageKey, null, eid, id, ep, sourceKey, epName); - ImageFavoritesEp? imageFavoritesEp = - imageFavoritesComic.imageFavoritesEp.firstWhereOrNull((e) { - return e.ep == ep; - }); + ImageFavorite imageFavorite = ImageFavorite( + page, + imageKey, + null, + eid, + id, + ep, + sourceKey, + epName, + ); + ImageFavoritesEp? imageFavoritesEp = imageFavoritesComic + .imageFavoritesEp + .firstWhereOrNull((e) { + return e.ep == ep; + }); if (imageFavoritesEp == null) { if (page != firstPage) { var copy = imageFavorite.copyWith( @@ -285,10 +293,20 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ); // 不是第一页的话, 自动塞一个封面进去 imageFavoritesEp = ImageFavoritesEp( - eid, ep, [copy, imageFavorite], epName, maxPage); + eid, + ep, + [copy, imageFavorite], + epName, + maxPage, + ); } else { - imageFavoritesEp = - ImageFavoritesEp(eid, ep, [imageFavorite], epName, maxPage); + imageFavoritesEp = ImageFavoritesEp( + eid, + ep, + [imageFavorite], + epName, + maxPage, + ); } imageFavoritesComic.imageFavoritesEp.add(imageFavoritesEp); } else { @@ -312,7 +330,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ImageFavoriteManager().addOrUpdateOrDelete(imageFavoritesComic); showToast( - message: "Successfully collected".tl, context: context, seconds: 1); + message: "Successfully collected".tl, + context: context, + seconds: 1, + ); } update(); } catch (e, stackTrace) { @@ -331,44 +352,53 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { height: kBottomBarHeight, child: Column( children: [ - const SizedBox( - height: 8, - ), + const SizedBox(height: 8), Row( children: [ const SizedBox(width: 8), IconButton.filledTonal( onPressed: () => !isReversed ? context.reader.chapter > 1 - ? context.reader.toPrevChapter() - : context.reader.toPage(1) + ? context.reader.toPrevChapter() + : context.reader.toPage( + context.reader.cid, + context.reader.type, + 1, + ) : context.reader.chapter < context.reader.maxChapter - ? context.reader.toNextChapter() - : context.reader.toPage(context.reader.maxPage), + ? context.reader.toNextChapter() + : context.reader.toPage( + context.reader.cid, + context.reader.type, + context.reader.maxPage, + ), icon: const Icon(Icons.first_page), ), - Expanded( - child: buildSlider(), - ), + Expanded(child: buildSlider()), IconButton.filledTonal( - onPressed: () => !isReversed - ? context.reader.chapter < context.reader.maxChapter + onPressed: () => !isReversed + ? context.reader.chapter < context.reader.maxChapter ? context.reader.toNextChapter() - : context.reader.toPage(context.reader.maxPage) - : context.reader.chapter > 1 - ? context.reader.toPrevChapter() - : context.reader.toPage(1), - icon: const Icon(Icons.last_page)), - const SizedBox( - width: 8, + : context.reader.toPage( + context.reader.cid, + context.reader.type, + context.reader.maxPage, + ) + : context.reader.chapter > 1 + ? context.reader.toPrevChapter() + : context.reader.toPage( + context.reader.cid, + context.reader.type, + 1, + ), + icon: const Icon(Icons.last_page), ), + const SizedBox(width: 8), ], ), Row( children: [ - const SizedBox( - width: 16, - ), + const SizedBox(width: 16), Container( height: 24, padding: const EdgeInsets.fromLTRB(6, 2, 6, 0), @@ -376,16 +406,15 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { color: Theme.of(context).colorScheme.tertiaryContainer, borderRadius: BorderRadius.circular(8), ), - child: Center( - child: Text(text), - ), + child: Center(child: Text(text)), ), const Spacer(), Tooltip( message: "Collect the image".tl, child: IconButton( - icon: - Icon(isLiked() ? Icons.favorite : Icons.favorite_border), + icon: Icon( + isLiked() ? Icons.favorite : Icons.favorite_border, + ), onPressed: addImageFavorite, ), ), @@ -427,14 +456,15 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { }); SystemChrome.setPreferredOrientations([ DeviceOrientation.landscapeLeft, - DeviceOrientation.landscapeRight + DeviceOrientation.landscapeRight, ]); } else { setState(() { rotation = null; }); SystemChrome.setPreferredOrientations( - DeviceOrientation.values); + DeviceOrientation.values, + ); } }, ), @@ -446,7 +476,10 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ? const Icon(Icons.timer) : const Icon(Icons.timer_sharp), onPressed: () { - context.reader.autoPageTurning(); + context.reader.autoPageTurning( + context.reader.cid, + context.reader.type, + ); update(); }, ), @@ -473,9 +506,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { onPressed: share, ), ), - const SizedBox(width: 4) + const SizedBox(width: 4), ], - ) + ), ], ), ); @@ -506,19 +539,26 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { focusNode: sliderFocus, value: context.reader.page.toDouble(), min: 1, - max: - context.reader.maxPage.clamp(context.reader.page, 1 << 16).toDouble(), + max: context.reader.maxPage + .clamp(context.reader.page, 1 << 16) + .toDouble(), reversed: isReversed, divisions: (context.reader.maxPage - 1).clamp(2, 1 << 16), onChanged: (i) { - context.reader.toPage(i.toInt()); + context.reader.toPage( + context.reader.cid, + context.reader.type, + i.toInt(), + ); }, ); } Widget buildPageInfoText() { - var epName = context.reader.widget.chapters?.titles - .elementAtOrNull(context.reader.chapter - 1) ?? + var epName = + context.reader.widget.chapters?.titles.elementAtOrNull( + context.reader.chapter - 1, + ) ?? "E${context.reader.chapter}"; if (epName.length > 8) { epName = "${epName.substring(0, 8)}..."; @@ -594,24 +634,35 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } var fileType = detectFileType(data); var filename = "${context.reader.page}${fileType.ext}"; - Share.shareFile( - data: data, - filename: filename, - mime: fileType.mime, - ); + Share.shareFile(data: data, filename: filename, mime: fileType.mime); } void openSetting() { showSideBar( context, ReaderSettings( + comicId: context.reader.cid, + comicSource: context.reader.type.sourceKey, onChanged: (key) { if (key == "readerMode") { - context.reader.mode = ReaderMode.fromKey(appdata.settings[key]); + context.reader.mode = ReaderMode.fromKey( + appdata.settings.getReaderSetting( + context.reader.cid, + context.reader.type.sourceKey, + key, + ), + ); } if (key == "enableTurnPageByVolumeKey") { - if (appdata.settings[key]) { - context.reader.handleVolumeEvent(); + if (appdata.settings.getReaderSetting( + context.reader.cid, + context.reader.type.sourceKey, + key, + )) { + context.reader.handleVolumeEvent( + context.reader.cid, + context.reader.type, + ); } else { context.reader.stopVolumeEvent(); } @@ -716,8 +767,8 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { return await File(imageKey.substring(7)).readAsBytes(); } else { return (await CacheManager().findCache( - "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}"))! - .readAsBytes(); + "$imageKey@${context.reader.type.sourceKey}@${context.reader.cid}@${context.reader.eid}", + ))!.readAsBytes(); } } @@ -733,14 +784,17 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { entry = OverlayEntry( builder: (context) { return Positioned.fill( - child: _SelectImageOverlayContent(onTap: (offset) { - completer.complete(offset); - entry!.remove(); - }, onDispose: () { - if (!completer.isCompleted) { - completer.complete(null); - } - }), + child: _SelectImageOverlayContent( + onTap: (offset) { + completer.complete(offset); + entry!.remove(); + }, + onDispose: () { + if (!completer.isCompleted) { + completer.complete(null); + } + }, + ), ); }, ); @@ -840,20 +894,17 @@ class _BatteryWidgetState extends State<_BatteryWidget> { size: 16, color: batteryColor, // Stroke - shadows: List.generate( - 9, - (index) { - if (index == 4) { - return null; - } - double offsetX = (index % 3 - 1) * 0.8; - double offsetY = ((index / 3).floor() - 1) * 0.8; - return Shadow( - color: context.colorScheme.onInverseSurface, - offset: Offset(offsetX, offsetY), - ); - }, - ).whereType().toList(), + shadows: List.generate(9, (index) { + if (index == 4) { + return null; + } + double offsetX = (index % 3 - 1) * 0.8; + double offsetY = ((index / 3).floor() - 1) * 0.8; + return Shadow( + color: context.colorScheme.onInverseSurface, + offset: Offset(offsetX, offsetY), + ); + }).whereType().toList(), ), Stack( children: [ @@ -940,10 +991,12 @@ class _SelectImageOverlayContent extends StatefulWidget { final void Function() onDispose; @override - State<_SelectImageOverlayContent> createState() => _SelectImageOverlayContentState(); + State<_SelectImageOverlayContent> createState() => + _SelectImageOverlayContentState(); } -class _SelectImageOverlayContentState extends State<_SelectImageOverlayContent> { +class _SelectImageOverlayContentState + extends State<_SelectImageOverlayContent> { @override void dispose() { widget.onDispose(); @@ -960,19 +1013,14 @@ class _SelectImageOverlayContentState extends State<_SelectImageOverlayContent> child: Container( color: Colors.black.withAlpha(50), child: Align( - alignment: Alignment( - 0, - -0.8, - ), + alignment: Alignment(0, -0.8), child: Container( width: 232, height: 42, decoration: BoxDecoration( color: context.colorScheme.surface, borderRadius: BorderRadius.circular(8), - border: Border.all( - color: context.colorScheme.outlineVariant, - ), + border: Border.all(color: context.colorScheme.outlineVariant), ), child: Row( children: [ diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index bcdd3db..770a3db 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -1,9 +1,16 @@ part of 'settings_page.dart'; class ReaderSettings extends StatefulWidget { - const ReaderSettings({super.key, this.onChanged}); + const ReaderSettings({ + super.key, + this.onChanged, + this.comicId, + this.comicSource, + }); final void Function(String key)? onChanged; + final String? comicId; + final String? comicSource; @override State createState() => _ReaderSettingsState(); @@ -21,6 +28,8 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call("enableTapToTurnPages"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), _SwitchSetting( title: "Reverse tap to turn Pages".tl, @@ -28,6 +37,8 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call("reverseTapToTurnPages"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), _SwitchSetting( title: "Page animation".tl, @@ -35,6 +46,15 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call("enablePageAnimation"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, + ).toSliver(), + _SwitchSetting( + title: "Enable comic specific settings".tl, + settingKey: "enableComicSpecificSettings", + onChanged: () { + widget.onChanged?.call("enableComicSpecificSettings"); + }, ).toSliver(), SelectSetting( title: "Reading mode".tl, @@ -58,6 +78,8 @@ class _ReaderSettingsState extends State { } widget.onChanged?.call("readerMode"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), _SliderSetting( title: "Auto page turning interval".tl, @@ -69,6 +91,8 @@ class _ReaderSettingsState extends State { setState(() {}); widget.onChanged?.call("autoPageTurningInterval"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), SliverAnimatedVisibility( visible: appdata.settings['readerMode']!.startsWith('gallery'), @@ -84,6 +108,8 @@ class _ReaderSettingsState extends State { setState(() {}); widget.onChanged?.call("readerScreenPicNumberForLandscape"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ), ), SliverAnimatedVisibility( @@ -99,10 +125,13 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call("readerScreenPicNumberForPortrait"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ), ), SliverAnimatedVisibility( - visible: appdata.settings['readerMode']!.startsWith('gallery') && + visible: + appdata.settings['readerMode']!.startsWith('gallery') && (appdata.settings['readerScreenPicNumberForLandscape'] > 1 || appdata.settings['readerScreenPicNumberForPortrait'] > 1), child: _SwitchSetting( @@ -111,6 +140,8 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call("showSingleImageOnFirstPage"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ), ), _SwitchSetting( @@ -120,6 +151,8 @@ class _ReaderSettingsState extends State { setState(() {}); widget.onChanged?.call('enableDoubleTapToZoom'); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), _SwitchSetting( title: 'Long press to zoom'.tl, @@ -128,6 +161,8 @@ class _ReaderSettingsState extends State { setState(() {}); widget.onChanged?.call('enableLongPressToZoom'); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), SliverAnimatedVisibility( visible: appdata.settings['enableLongPressToZoom'] == true, @@ -138,6 +173,8 @@ class _ReaderSettingsState extends State { "press": "Press position".tl, "center": "Screen center".tl, }, + comicId: widget.comicId, + comicSource: widget.comicSource, ), ), _SwitchSetting( @@ -147,6 +184,8 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call('limitImageWidth'); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), if (App.isAndroid) _SwitchSetting( @@ -155,6 +194,8 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call('enableTurnPageByVolumeKey'); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), _SwitchSetting( title: "Display time & battery info in reader".tl, @@ -162,6 +203,8 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call("enableClockAndBatteryInfoInReader"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), _SwitchSetting( title: "Show system status bar".tl, @@ -169,6 +212,8 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call("showSystemStatusBar"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), SelectSetting( title: "Quick collect image".tl, @@ -184,6 +229,8 @@ class _ReaderSettingsState extends State { help: "On the image browsing page, you can quickly collect images by sliding horizontally or vertically according to your reading mode" .tl, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), _CallbackSetting( title: "Custom Image Processing".tl, @@ -196,6 +243,8 @@ class _ReaderSettingsState extends State { interval: 1, min: 1, max: 16, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), _SwitchSetting( title: "Show Page Number".tl, @@ -203,7 +252,39 @@ class _ReaderSettingsState extends State { onChanged: () { widget.onChanged?.call("showPageNumberInReader"); }, + comicId: widget.comicId, + comicSource: widget.comicSource, ).toSliver(), + // reset button + SliverToBoxAdapter( + child: TextButton( + onPressed: () { + if (widget.comicId == null) { + appdata.settings.resetAllComicReaderSettings(); + } else { + var keys = appdata + .settings['comicSpecificSettings']["${widget.comicId}@${widget.comicSource}"] + ?.keys; + appdata.settings.resetComicReaderSettings( + widget.comicId!, + widget.comicSource!, + ); + if (keys != null) { + setState(() {}); + for (var key in keys) { + widget.onChanged?.call(key); + } + } + } + }, + child: Text( + (widget.comicId == null + ? "Clear specific reader settings for all comics" + : "Clear specific reader settings for this comic") + .tl, + ), + ), + ), ], ); } @@ -248,7 +329,7 @@ class __CustomImageProcessingState extends State<_CustomImageProcessing> { setState(() {}); }, child: Text("Reset".tl), - ) + ), ], ), body: Column( @@ -274,7 +355,7 @@ class __CustomImageProcessingState extends State<_CustomImageProcessing> { ), ), ), - ) + ), ], ), ); diff --git a/lib/pages/settings/setting_components.dart b/lib/pages/settings/setting_components.dart index a842a1f..f70e7d1 100644 --- a/lib/pages/settings/setting_components.dart +++ b/lib/pages/settings/setting_components.dart @@ -6,6 +6,8 @@ class _SwitchSetting extends StatefulWidget { required this.settingKey, this.onChanged, this.subtitle, + this.comicId, + this.comicSource, }); final String title; @@ -16,6 +18,10 @@ class _SwitchSetting extends StatefulWidget { final String? subtitle; + final String? comicId; + + final String? comicSource; + @override State<_SwitchSetting> createState() => _SwitchSettingState(); } @@ -23,16 +29,33 @@ class _SwitchSetting extends StatefulWidget { class _SwitchSettingState extends State<_SwitchSetting> { @override Widget build(BuildContext context) { - assert(appdata.settings[widget.settingKey] is bool); + var value = widget.comicId == null + ? appdata.settings[widget.settingKey] + : appdata.settings.getReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingKey, + ); + + assert(value is bool); return ListTile( title: Text(widget.title), subtitle: widget.subtitle == null ? null : Text(widget.subtitle!), trailing: Switch( - value: appdata.settings[widget.settingKey], + value: value, onChanged: (value) { setState(() { - appdata.settings[widget.settingKey] = value; + if (widget.comicId == null) { + appdata.settings[widget.settingKey] = value; + } else { + appdata.settings.setReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingKey, + value, + ); + } }); appdata.saveData().then((_) { widget.onChanged?.call(); @@ -51,6 +74,8 @@ class SelectSetting extends StatelessWidget { required this.optionTranslation, this.onChanged, this.help, + this.comicId, + this.comicSource, }); final String title; @@ -63,6 +88,10 @@ class SelectSetting extends StatelessWidget { final String? help; + final String? comicId; + + final String? comicSource; + @override Widget build(BuildContext context) { return SizedBox( @@ -76,6 +105,8 @@ class SelectSetting extends StatelessWidget { optionTranslation: optionTranslation, onChanged: onChanged, help: help, + comicId: comicId, + comicSource: comicSource, ); } else { return _EndSelectorSelectSetting( @@ -84,6 +115,8 @@ class SelectSetting extends StatelessWidget { optionTranslation: optionTranslation, onChanged: onChanged, help: help, + comicId: comicId, + comicSource: comicSource, ); } }, @@ -99,6 +132,8 @@ class _DoubleLineSelectSettings extends StatefulWidget { required this.optionTranslation, this.onChanged, this.help, + this.comicId, + this.comicSource, }); final String title; @@ -111,6 +146,10 @@ class _DoubleLineSelectSettings extends StatefulWidget { final String? help; + final String? comicId; + + final String? comicSource; + @override State<_DoubleLineSelectSettings> createState() => _DoubleLineSelectSettingsState(); @@ -119,6 +158,14 @@ class _DoubleLineSelectSettings extends StatefulWidget { class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> { @override Widget build(BuildContext context) { + var value = widget.comicId == null + ? appdata.settings[widget.settingKey] + : appdata.settings.getReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingKey, + ); + return ListTile( title: Row( children: [ @@ -134,9 +181,9 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> { builder: (context) { return ContentDialog( title: "Help".tl, - content: Text(widget.help!) - .paddingHorizontal(16) - .fixWidth(double.infinity), + content: Text( + widget.help!, + ).paddingHorizontal(16).fixWidth(double.infinity), actions: [ Button.filled( onPressed: context.pop, @@ -150,9 +197,7 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> { ), ], ), - subtitle: Text( - widget.optionTranslation[appdata.settings[widget.settingKey]] ?? - "None"), + subtitle: Text(widget.optionTranslation[value] ?? "None"), trailing: const Icon(Icons.arrow_drop_down), onTap: () { var renderBox = context.findRenderObject() as RenderBox; @@ -170,16 +215,27 @@ class _DoubleLineSelectSettingsState extends State<_DoubleLineSelectSettings> { Offset.zero & MediaQuery.of(context).size, ), items: widget.optionTranslation.keys - .map((key) => PopupMenuItem( - value: key, - height: App.isMobile ? 46 : 40, - child: Text(widget.optionTranslation[key]!), - )) + .map( + (key) => PopupMenuItem( + value: key, + height: App.isMobile ? 46 : 40, + child: Text(widget.optionTranslation[key]!), + ), + ) .toList(), ).then((value) { if (value != null) { setState(() { - appdata.settings[widget.settingKey] = value; + if (widget.comicId == null) { + appdata.settings[widget.settingKey] = value; + } else { + appdata.settings.setReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingKey, + value, + ); + } }); appdata.saveData(); widget.onChanged?.call(); @@ -197,6 +253,8 @@ class _EndSelectorSelectSetting extends StatefulWidget { required this.optionTranslation, this.onChanged, this.help, + this.comicId, + this.comicSource, }); final String title; @@ -209,6 +267,10 @@ class _EndSelectorSelectSetting extends StatefulWidget { final String? help; + final String? comicId; + + final String? comicSource; + @override State<_EndSelectorSelectSetting> createState() => _EndSelectorSelectSettingState(); @@ -218,6 +280,13 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> { @override Widget build(BuildContext context) { var options = widget.optionTranslation; + var value = widget.comicId == null + ? appdata.settings[widget.settingKey] + : appdata.settings.getReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingKey, + ); return ListTile( title: Row( children: [ @@ -233,9 +302,9 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> { builder: (context) { return ContentDialog( title: "Help".tl, - content: Text(widget.help!) - .paddingHorizontal(16) - .fixWidth(double.infinity), + content: Text( + widget.help!, + ).paddingHorizontal(16).fixWidth(double.infinity), actions: [ Button.filled( onPressed: context.pop, @@ -250,12 +319,22 @@ class _EndSelectorSelectSettingState extends State<_EndSelectorSelectSetting> { ], ), trailing: Select( - current: options[appdata.settings[widget.settingKey]], + current: options[value], values: options.values.toList(), minWidth: 64, onTap: (index) { setState(() { - appdata.settings[widget.settingKey] = options.keys.elementAt(index); + var value = options.keys.elementAt(index); + if (widget.comicId == null) { + appdata.settings[widget.settingKey] = value; + } else { + appdata.settings.setReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingKey, + value, + ); + } }); appdata.saveData(); widget.onChanged?.call(); @@ -273,6 +352,8 @@ class _SliderSetting extends StatefulWidget { required this.min, required this.max, this.onChanged, + this.comicId, + this.comicSource, }); final String title; @@ -287,6 +368,10 @@ class _SliderSetting extends StatefulWidget { final VoidCallback? onChanged; + final String? comicId; + + final String? comicSource; + @override State<_SliderSetting> createState() => _SliderSettingState(); } @@ -294,28 +379,52 @@ class _SliderSetting extends StatefulWidget { class _SliderSettingState extends State<_SliderSetting> { @override Widget build(BuildContext context) { + var value = + (widget.comicId == null + ? appdata.settings[widget.settingsIndex] + : appdata.settings.getReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingsIndex, + )) + .toDouble(); return ListTile( title: Row( children: [ Text(widget.title), const Spacer(), - Text( - appdata.settings[widget.settingsIndex].toString(), - style: ts.s12, - ), + Text(value.toString(), style: ts.s12), ], ), subtitle: Slider( - value: appdata.settings[widget.settingsIndex].toDouble(), + value: value, onChanged: (value) { if (value.toInt() == value) { setState(() { - appdata.settings[widget.settingsIndex] = value.toInt(); + if (widget.comicId == null) { + appdata.settings[widget.settingsIndex] = value.toInt(); + } else { + appdata.settings.setReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingsIndex, + value.toInt(), + ); + } appdata.saveData(); }); } else { setState(() { - appdata.settings[widget.settingsIndex] = value; + if (widget.comicId == null) { + appdata.settings[widget.settingsIndex] = value; + } else { + appdata.settings.setReaderSetting( + widget.comicId!, + widget.comicSource!, + widget.settingsIndex, + value, + ); + } appdata.saveData(); }); } @@ -402,10 +511,11 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> { color: Theme.of(context).colorScheme.surfaceContainer, boxShadow: const [ BoxShadow( - color: Colors.black12, - blurRadius: 5, - offset: Offset(0, 2), - spreadRadius: 2) + color: Colors.black12, + blurRadius: 5, + offset: Offset(0, 2), + spreadRadius: 2, + ), ], ), onReorder: (reorderFunc) { @@ -435,7 +545,7 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> { label: Text("Add".tl), icon: const Icon(Icons.add), onPressed: showAddDialog, - ) + ), ], body: view, ); @@ -445,12 +555,13 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> { Widget removeButton = Padding( padding: const EdgeInsets.only(right: 8), child: IconButton( - onPressed: () { - setState(() { - keys.remove(key); - }); - }, - icon: const Icon(Icons.delete_outline)), + onPressed: () { + setState(() { + keys.remove(key); + }); + }, + icon: const Icon(Icons.delete_outline), + ), ); return ListTile( @@ -458,10 +569,7 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> { key: Key(key), trailing: Row( mainAxisSize: MainAxisSize.min, - children: [ - removeButton, - const Icon(Icons.drag_handle), - ], + children: [removeButton, const Icon(Icons.drag_handle)], ), ); } @@ -477,64 +585,66 @@ class _MultiPagesFilterState extends State<_MultiPagesFilter> { showDialog( context: context, builder: (context) { - return StatefulBuilder(builder: (context, setState) { - return ContentDialog( - title: "Add".tl, - content: Column( - mainAxisSize: MainAxisSize.min, - children: canAdd.entries - .map( - (e) => CheckboxListTile( - value: selected.contains(e.key), - title: Text(e.value), - key: Key(e.key), - onChanged: (value) { - setState(() { - if (value!) { - selected.add(e.key); - } else { - selected.remove(e.key); - } - }); - }, - ), - ) - .toList(), - ), - actions: [ - if (selected.length < canAdd.length) - TextButton( - child: Text("Select All".tl), - onPressed: () { - setState(() { - selected = canAdd.keys.toList(); - }); - }, - ) - else - TextButton( - child: Text("Deselect All".tl), - onPressed: () { - setState(() { - selected.clear(); - }); - }, - ), - const SizedBox(width: 8), - FilledButton( - onPressed: selected.isNotEmpty - ? () { - this.setState(() { - keys.addAll(selected); - }); - Navigator.pop(context); - } - : null, - child: Text("Add".tl), + return StatefulBuilder( + builder: (context, setState) { + return ContentDialog( + title: "Add".tl, + content: Column( + mainAxisSize: MainAxisSize.min, + children: canAdd.entries + .map( + (e) => CheckboxListTile( + value: selected.contains(e.key), + title: Text(e.value), + key: Key(e.key), + onChanged: (value) { + setState(() { + if (value!) { + selected.add(e.key); + } else { + selected.remove(e.key); + } + }); + }, + ), + ) + .toList(), ), - ], - ); - }); + actions: [ + if (selected.length < canAdd.length) + TextButton( + child: Text("Select All".tl), + onPressed: () { + setState(() { + selected = canAdd.keys.toList(); + }); + }, + ) + else + TextButton( + child: Text("Deselect All".tl), + onPressed: () { + setState(() { + selected.clear(); + }); + }, + ), + const SizedBox(width: 8), + FilledButton( + onPressed: selected.isNotEmpty + ? () { + this.setState(() { + keys.addAll(selected); + }); + Navigator.pop(context); + } + : null, + child: Text("Add".tl), + ), + ], + ); + }, + ); }, ); }