diff --git a/lib/eval/lnreader/js_htmlparser.dart b/lib/eval/lnreader/js_htmlparser.dart index fded1f93..74ae4b0e 100644 --- a/lib/eval/lnreader/js_htmlparser.dart +++ b/lib/eval/lnreader/js_htmlparser.dart @@ -160,9 +160,9 @@ class Parser { } else { // attribute without value (e.g. `disabled`) if (this.options.onattribute) { - this.options.onattribute(attrName, null); + this.options.onattribute(attrName, ""); } - attrs[attrName] = null; + attrs[attrName] = ""; attrName = ''; } } diff --git a/lib/main.dart b/lib/main.dart index 171087d7..70d38f79 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -22,6 +22,7 @@ import 'package:mangayomi/models/track.dart' as track; import 'package:mangayomi/models/track_preference.dart'; import 'package:mangayomi/models/track_search.dart'; import 'package:mangayomi/modules/manga/detail/providers/track_state_providers.dart'; +import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart'; import 'package:mangayomi/modules/more/data_and_storage/providers/storage_usage.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/modules/more/settings/general/providers/general_state_provider.dart'; @@ -31,6 +32,7 @@ import 'package:mangayomi/router/router.dart'; import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart'; import 'package:mangayomi/l10n/generated/app_localizations.dart'; import 'package:mangayomi/services/http/m_client.dart'; +import 'package:mangayomi/services/isolate_service.dart'; import 'package:mangayomi/src/rust/frb_generated.dart'; import 'package:mangayomi/utils/discord_rpc.dart'; import 'package:mangayomi/utils/log/logger.dart'; @@ -52,6 +54,8 @@ void main(List args) async { if (Platform.isLinux && runWebViewTitleBarWidget(args)) return; MediaKit.ensureInitialized(); await RustLib.init(); + await imgCropIsolate.start(); + await getIsolateService.start(); if (!(Platform.isAndroid || Platform.isIOS)) { await windowManager.ensureInitialized(); } diff --git a/lib/models/settings.dart b/lib/models/settings.dart index 5a4ec726..80d736dd 100644 --- a/lib/models/settings.dart +++ b/lib/models/settings.dart @@ -244,6 +244,22 @@ class Settings { @enumerated late NovelTextAlign novelTextAlign; + String? novelReaderTheme; + + String? novelReaderTextColor; + + int? novelReaderPadding; + + double? novelReaderLineHeight; + + bool? novelShowScrollPercentage; + + bool? novelAutoScroll; + + bool? novelRemoveExtraParagraphSpacing; + + bool? novelTapToScroll; + List? navigationOrder; List? hideItems; @@ -392,6 +408,14 @@ class Settings { this.novelDisplayType = DisplayType.comfortableGrid, this.novelFontSize = 14, this.novelTextAlign = NovelTextAlign.left, + this.novelReaderTheme = '#292832', + this.novelReaderTextColor = '#CCCCCC', + this.novelReaderPadding = 16, + this.novelReaderLineHeight = 1.5, + this.novelShowScrollPercentage = true, + this.novelAutoScroll = false, + this.novelRemoveExtraParagraphSpacing = false, + this.novelTapToScroll = false, this.navigationOrder, this.hideItems, this.clearChapterCacheOnAppLaunch = false, @@ -605,6 +629,22 @@ class Settings { } novelTextAlign = NovelTextAlign .values[json['novelTextAlign'] ?? NovelTextAlign.left.index]; + if (json['novelReaderTheme'] != null) { + novelReaderTheme = json['novelReaderTheme']; + } + if (json['novelReaderTextColor'] != null) { + novelReaderTextColor = json['novelReaderTextColor']; + } + if (json['novelReaderPadding'] != null) { + novelReaderPadding = json['novelReaderPadding']; + } + if (json['novelReaderLineHeight'] != null) { + novelReaderLineHeight = json['novelReaderLineHeight']; + } + novelShowScrollPercentage = json['novelShowScrollPercentage']; + novelAutoScroll = json['novelAutoScroll']; + novelRemoveExtraParagraphSpacing = json['novelRemoveExtraParagraphSpacing']; + novelTapToScroll = json['novelTapToScroll']; if (json['navigationOrder'] != null) { navigationOrder = (json['navigationOrder'] as List).cast(); } @@ -783,6 +823,14 @@ class Settings { 'novelDisplayType': novelDisplayType.index, 'novelFontSize': novelFontSize, 'novelTextAlign': novelTextAlign.index, + 'novelReaderTheme': novelReaderTheme, + 'novelReaderTextColor': novelReaderTextColor, + 'novelReaderPadding': novelReaderPadding, + 'novelReaderLineHeight': novelReaderLineHeight, + 'novelShowScrollPercentage': novelShowScrollPercentage, + 'novelAutoScroll': novelAutoScroll, + 'novelRemoveExtraParagraphSpacing': novelRemoveExtraParagraphSpacing, + 'novelTapToScroll': novelTapToScroll, 'navigationOrder': navigationOrder, 'hideItems': hideItems, 'clearChapterCacheOnAppLaunch': clearChapterCacheOnAppLaunch, diff --git a/lib/models/settings.g.dart b/lib/models/settings.g.dart index 53bfa3ab..5de72b6c 100644 --- a/lib/models/settings.g.dart +++ b/lib/models/settings.g.dart @@ -529,212 +529,252 @@ const SettingsSchema = CollectionSchema( name: r'navigationOrder', type: IsarType.stringList, ), - r'novelDisplayType': PropertySchema( + r'novelAutoScroll': PropertySchema( id: 95, + name: r'novelAutoScroll', + type: IsarType.bool, + ), + r'novelDisplayType': PropertySchema( + id: 96, name: r'novelDisplayType', type: IsarType.byte, enumMap: _SettingsnovelDisplayTypeEnumValueMap, ), r'novelExtensionsRepo': PropertySchema( - id: 96, + id: 97, name: r'novelExtensionsRepo', type: IsarType.objectList, target: r'Repo', ), r'novelFontSize': PropertySchema( - id: 97, + id: 98, name: r'novelFontSize', type: IsarType.long, ), r'novelGridSize': PropertySchema( - id: 98, + id: 99, name: r'novelGridSize', type: IsarType.long, ), r'novelLibraryDownloadedChapters': PropertySchema( - id: 99, + id: 100, name: r'novelLibraryDownloadedChapters', type: IsarType.bool, ), r'novelLibraryLocalSource': PropertySchema( - id: 100, + id: 101, name: r'novelLibraryLocalSource', type: IsarType.bool, ), r'novelLibraryShowCategoryTabs': PropertySchema( - id: 101, + id: 102, name: r'novelLibraryShowCategoryTabs', type: IsarType.bool, ), r'novelLibraryShowContinueReadingButton': PropertySchema( - id: 102, + id: 103, name: r'novelLibraryShowContinueReadingButton', type: IsarType.bool, ), r'novelLibraryShowLanguage': PropertySchema( - id: 103, + id: 104, name: r'novelLibraryShowLanguage', type: IsarType.bool, ), r'novelLibraryShowNumbersOfItems': PropertySchema( - id: 104, + id: 105, name: r'novelLibraryShowNumbersOfItems', type: IsarType.bool, ), + r'novelReaderLineHeight': PropertySchema( + id: 106, + name: r'novelReaderLineHeight', + type: IsarType.double, + ), + r'novelReaderPadding': PropertySchema( + id: 107, + name: r'novelReaderPadding', + type: IsarType.long, + ), + r'novelReaderTextColor': PropertySchema( + id: 108, + name: r'novelReaderTextColor', + type: IsarType.string, + ), + r'novelReaderTheme': PropertySchema( + id: 109, + name: r'novelReaderTheme', + type: IsarType.string, + ), + r'novelRemoveExtraParagraphSpacing': PropertySchema( + id: 110, + name: r'novelRemoveExtraParagraphSpacing', + type: IsarType.bool, + ), + r'novelShowScrollPercentage': PropertySchema( + id: 111, + name: r'novelShowScrollPercentage', + type: IsarType.bool, + ), + r'novelTapToScroll': PropertySchema( + id: 112, + name: r'novelTapToScroll', + type: IsarType.bool, + ), r'novelTextAlign': PropertySchema( - id: 105, + id: 113, name: r'novelTextAlign', type: IsarType.byte, enumMap: _SettingsnovelTextAlignEnumValueMap, ), r'onlyIncludePinnedSources': PropertySchema( - id: 106, + id: 114, name: r'onlyIncludePinnedSources', type: IsarType.bool, ), r'pagePreloadAmount': PropertySchema( - id: 107, + id: 115, name: r'pagePreloadAmount', type: IsarType.long, ), r'personalPageModeList': PropertySchema( - id: 108, + id: 116, name: r'personalPageModeList', type: IsarType.objectList, target: r'PersonalPageMode', ), r'personalReaderModeList': PropertySchema( - id: 109, + id: 117, name: r'personalReaderModeList', type: IsarType.objectList, target: r'PersonalReaderMode', ), r'playerSubtitleSettings': PropertySchema( - id: 110, + id: 118, name: r'playerSubtitleSettings', type: IsarType.object, target: r'PlayerSubtitleSettings', ), r'pureBlackDarkMode': PropertySchema( - id: 111, + id: 119, name: r'pureBlackDarkMode', type: IsarType.bool, ), r'relativeTimesTamps': PropertySchema( - id: 112, + id: 120, name: r'relativeTimesTamps', type: IsarType.long, ), r'rpcShowCoverImage': PropertySchema( - id: 113, + id: 121, name: r'rpcShowCoverImage', type: IsarType.bool, ), r'rpcShowReadingWatchingProgress': PropertySchema( - id: 114, + id: 122, name: r'rpcShowReadingWatchingProgress', type: IsarType.bool, ), r'rpcShowTitle': PropertySchema( - id: 115, + id: 123, name: r'rpcShowTitle', type: IsarType.bool, ), r'saveAsCBZArchive': PropertySchema( - id: 116, + id: 124, name: r'saveAsCBZArchive', type: IsarType.bool, ), r'scaleType': PropertySchema( - id: 117, + id: 125, name: r'scaleType', type: IsarType.byte, enumMap: _SettingsscaleTypeEnumValueMap, ), r'showPagesNumber': PropertySchema( - id: 118, + id: 126, name: r'showPagesNumber', type: IsarType.bool, ), r'sortChapterList': PropertySchema( - id: 119, + id: 127, name: r'sortChapterList', type: IsarType.objectList, target: r'SortChapter', ), r'sortLibraryAnime': PropertySchema( - id: 120, + id: 128, name: r'sortLibraryAnime', type: IsarType.object, target: r'SortLibraryManga', ), r'sortLibraryManga': PropertySchema( - id: 121, + id: 129, name: r'sortLibraryManga', type: IsarType.object, target: r'SortLibraryManga', ), r'sortLibraryNovel': PropertySchema( - id: 122, + id: 130, name: r'sortLibraryNovel', type: IsarType.object, target: r'SortLibraryManga', ), r'startDatebackup': PropertySchema( - id: 123, + id: 131, name: r'startDatebackup', type: IsarType.long, ), r'themeIsDark': PropertySchema( - id: 124, + id: 132, name: r'themeIsDark', type: IsarType.bool, ), r'updateProgressAfterReading': PropertySchema( - id: 125, + id: 133, name: r'updateProgressAfterReading', type: IsarType.bool, ), r'updatedAt': PropertySchema( - id: 126, + id: 134, name: r'updatedAt', type: IsarType.long, ), r'useLibass': PropertySchema( - id: 127, + id: 135, name: r'useLibass', type: IsarType.bool, ), r'useMpvConfig': PropertySchema( - id: 128, + id: 136, name: r'useMpvConfig', type: IsarType.bool, ), r'usePageTapZones': PropertySchema( - id: 129, + id: 137, name: r'usePageTapZones', type: IsarType.bool, ), r'useYUV420P': PropertySchema( - id: 130, + id: 138, name: r'useYUV420P', type: IsarType.bool, ), r'userAgent': PropertySchema( - id: 131, + id: 139, name: r'userAgent', type: IsarType.string, ), r'volumeBoostCap': PropertySchema( - id: 132, + id: 140, name: r'volumeBoostCap', type: IsarType.long, ), @@ -1107,6 +1147,18 @@ int _settingsEstimateSize( } } } + { + final value = object.novelReaderTextColor; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.novelReaderTheme; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } { final list = object.personalPageModeList; if (list != null) { @@ -1386,84 +1438,92 @@ void _settingsSerialize( writer.writeLong(offsets[92], object.markEpisodeAsSeenType); writer.writeBool(offsets[93], object.mergeLibraryNavMobile); writer.writeStringList(offsets[94], object.navigationOrder); - writer.writeByte(offsets[95], object.novelDisplayType.index); + writer.writeBool(offsets[95], object.novelAutoScroll); + writer.writeByte(offsets[96], object.novelDisplayType.index); writer.writeObjectList( - offsets[96], + offsets[97], allOffsets, RepoSchema.serialize, object.novelExtensionsRepo, ); - writer.writeLong(offsets[97], object.novelFontSize); - writer.writeLong(offsets[98], object.novelGridSize); - writer.writeBool(offsets[99], object.novelLibraryDownloadedChapters); - writer.writeBool(offsets[100], object.novelLibraryLocalSource); - writer.writeBool(offsets[101], object.novelLibraryShowCategoryTabs); - writer.writeBool(offsets[102], object.novelLibraryShowContinueReadingButton); - writer.writeBool(offsets[103], object.novelLibraryShowLanguage); - writer.writeBool(offsets[104], object.novelLibraryShowNumbersOfItems); - writer.writeByte(offsets[105], object.novelTextAlign.index); - writer.writeBool(offsets[106], object.onlyIncludePinnedSources); - writer.writeLong(offsets[107], object.pagePreloadAmount); + writer.writeLong(offsets[98], object.novelFontSize); + writer.writeLong(offsets[99], object.novelGridSize); + writer.writeBool(offsets[100], object.novelLibraryDownloadedChapters); + writer.writeBool(offsets[101], object.novelLibraryLocalSource); + writer.writeBool(offsets[102], object.novelLibraryShowCategoryTabs); + writer.writeBool(offsets[103], object.novelLibraryShowContinueReadingButton); + writer.writeBool(offsets[104], object.novelLibraryShowLanguage); + writer.writeBool(offsets[105], object.novelLibraryShowNumbersOfItems); + writer.writeDouble(offsets[106], object.novelReaderLineHeight); + writer.writeLong(offsets[107], object.novelReaderPadding); + writer.writeString(offsets[108], object.novelReaderTextColor); + writer.writeString(offsets[109], object.novelReaderTheme); + writer.writeBool(offsets[110], object.novelRemoveExtraParagraphSpacing); + writer.writeBool(offsets[111], object.novelShowScrollPercentage); + writer.writeBool(offsets[112], object.novelTapToScroll); + writer.writeByte(offsets[113], object.novelTextAlign.index); + writer.writeBool(offsets[114], object.onlyIncludePinnedSources); + writer.writeLong(offsets[115], object.pagePreloadAmount); writer.writeObjectList( - offsets[108], + offsets[116], allOffsets, PersonalPageModeSchema.serialize, object.personalPageModeList, ); writer.writeObjectList( - offsets[109], + offsets[117], allOffsets, PersonalReaderModeSchema.serialize, object.personalReaderModeList, ); writer.writeObject( - offsets[110], + offsets[118], allOffsets, PlayerSubtitleSettingsSchema.serialize, object.playerSubtitleSettings, ); - writer.writeBool(offsets[111], object.pureBlackDarkMode); - writer.writeLong(offsets[112], object.relativeTimesTamps); - writer.writeBool(offsets[113], object.rpcShowCoverImage); - writer.writeBool(offsets[114], object.rpcShowReadingWatchingProgress); - writer.writeBool(offsets[115], object.rpcShowTitle); - writer.writeBool(offsets[116], object.saveAsCBZArchive); - writer.writeByte(offsets[117], object.scaleType.index); - writer.writeBool(offsets[118], object.showPagesNumber); + writer.writeBool(offsets[119], object.pureBlackDarkMode); + writer.writeLong(offsets[120], object.relativeTimesTamps); + writer.writeBool(offsets[121], object.rpcShowCoverImage); + writer.writeBool(offsets[122], object.rpcShowReadingWatchingProgress); + writer.writeBool(offsets[123], object.rpcShowTitle); + writer.writeBool(offsets[124], object.saveAsCBZArchive); + writer.writeByte(offsets[125], object.scaleType.index); + writer.writeBool(offsets[126], object.showPagesNumber); writer.writeObjectList( - offsets[119], + offsets[127], allOffsets, SortChapterSchema.serialize, object.sortChapterList, ); writer.writeObject( - offsets[120], + offsets[128], allOffsets, SortLibraryMangaSchema.serialize, object.sortLibraryAnime, ); writer.writeObject( - offsets[121], + offsets[129], allOffsets, SortLibraryMangaSchema.serialize, object.sortLibraryManga, ); writer.writeObject( - offsets[122], + offsets[130], allOffsets, SortLibraryMangaSchema.serialize, object.sortLibraryNovel, ); - writer.writeLong(offsets[123], object.startDatebackup); - writer.writeBool(offsets[124], object.themeIsDark); - writer.writeBool(offsets[125], object.updateProgressAfterReading); - writer.writeLong(offsets[126], object.updatedAt); - writer.writeBool(offsets[127], object.useLibass); - writer.writeBool(offsets[128], object.useMpvConfig); - writer.writeBool(offsets[129], object.usePageTapZones); - writer.writeBool(offsets[130], object.useYUV420P); - writer.writeString(offsets[131], object.userAgent); - writer.writeLong(offsets[132], object.volumeBoostCap); + writer.writeLong(offsets[131], object.startDatebackup); + writer.writeBool(offsets[132], object.themeIsDark); + writer.writeBool(offsets[133], object.updateProgressAfterReading); + writer.writeLong(offsets[134], object.updatedAt); + writer.writeBool(offsets[135], object.useLibass); + writer.writeBool(offsets[136], object.useMpvConfig); + writer.writeBool(offsets[137], object.usePageTapZones); + writer.writeBool(offsets[138], object.useYUV420P); + writer.writeString(offsets[139], object.userAgent); + writer.writeLong(offsets[140], object.volumeBoostCap); } Settings _settingsDeserialize( @@ -1641,89 +1701,97 @@ Settings _settingsDeserialize( markEpisodeAsSeenType: reader.readLongOrNull(offsets[92]), mergeLibraryNavMobile: reader.readBoolOrNull(offsets[93]), navigationOrder: reader.readStringList(offsets[94]), + novelAutoScroll: reader.readBoolOrNull(offsets[95]), novelDisplayType: _SettingsnovelDisplayTypeValueEnumMap[reader.readByteOrNull( - offsets[95], + offsets[96], )] ?? DisplayType.comfortableGrid, novelExtensionsRepo: reader.readObjectList( - offsets[96], + offsets[97], RepoSchema.deserialize, allOffsets, Repo(), ), - novelFontSize: reader.readLongOrNull(offsets[97]), - novelLibraryDownloadedChapters: reader.readBoolOrNull(offsets[99]), - novelLibraryLocalSource: reader.readBoolOrNull(offsets[100]), - novelLibraryShowCategoryTabs: reader.readBoolOrNull(offsets[101]), - novelLibraryShowContinueReadingButton: reader.readBoolOrNull(offsets[102]), - novelLibraryShowLanguage: reader.readBoolOrNull(offsets[103]), - novelLibraryShowNumbersOfItems: reader.readBoolOrNull(offsets[104]), + novelFontSize: reader.readLongOrNull(offsets[98]), + novelLibraryDownloadedChapters: reader.readBoolOrNull(offsets[100]), + novelLibraryLocalSource: reader.readBoolOrNull(offsets[101]), + novelLibraryShowCategoryTabs: reader.readBoolOrNull(offsets[102]), + novelLibraryShowContinueReadingButton: reader.readBoolOrNull(offsets[103]), + novelLibraryShowLanguage: reader.readBoolOrNull(offsets[104]), + novelLibraryShowNumbersOfItems: reader.readBoolOrNull(offsets[105]), + novelReaderLineHeight: reader.readDoubleOrNull(offsets[106]), + novelReaderPadding: reader.readLongOrNull(offsets[107]), + novelReaderTextColor: reader.readStringOrNull(offsets[108]), + novelReaderTheme: reader.readStringOrNull(offsets[109]), + novelRemoveExtraParagraphSpacing: reader.readBoolOrNull(offsets[110]), + novelShowScrollPercentage: reader.readBoolOrNull(offsets[111]), + novelTapToScroll: reader.readBoolOrNull(offsets[112]), novelTextAlign: _SettingsnovelTextAlignValueEnumMap[reader.readByteOrNull( - offsets[105], + offsets[113], )] ?? NovelTextAlign.left, - onlyIncludePinnedSources: reader.readBoolOrNull(offsets[106]), - pagePreloadAmount: reader.readLongOrNull(offsets[107]), + onlyIncludePinnedSources: reader.readBoolOrNull(offsets[114]), + pagePreloadAmount: reader.readLongOrNull(offsets[115]), personalPageModeList: reader.readObjectList( - offsets[108], + offsets[116], PersonalPageModeSchema.deserialize, allOffsets, PersonalPageMode(), ), personalReaderModeList: reader.readObjectList( - offsets[109], + offsets[117], PersonalReaderModeSchema.deserialize, allOffsets, PersonalReaderMode(), ), playerSubtitleSettings: reader.readObjectOrNull( - offsets[110], + offsets[118], PlayerSubtitleSettingsSchema.deserialize, allOffsets, ), - pureBlackDarkMode: reader.readBoolOrNull(offsets[111]), - relativeTimesTamps: reader.readLongOrNull(offsets[112]), - rpcShowCoverImage: reader.readBoolOrNull(offsets[113]), - rpcShowReadingWatchingProgress: reader.readBoolOrNull(offsets[114]), - rpcShowTitle: reader.readBoolOrNull(offsets[115]), - saveAsCBZArchive: reader.readBoolOrNull(offsets[116]), + pureBlackDarkMode: reader.readBoolOrNull(offsets[119]), + relativeTimesTamps: reader.readLongOrNull(offsets[120]), + rpcShowCoverImage: reader.readBoolOrNull(offsets[121]), + rpcShowReadingWatchingProgress: reader.readBoolOrNull(offsets[122]), + rpcShowTitle: reader.readBoolOrNull(offsets[123]), + saveAsCBZArchive: reader.readBoolOrNull(offsets[124]), scaleType: - _SettingsscaleTypeValueEnumMap[reader.readByteOrNull(offsets[117])] ?? + _SettingsscaleTypeValueEnumMap[reader.readByteOrNull(offsets[125])] ?? ScaleType.fitScreen, - showPagesNumber: reader.readBoolOrNull(offsets[118]), + showPagesNumber: reader.readBoolOrNull(offsets[126]), sortChapterList: reader.readObjectList( - offsets[119], + offsets[127], SortChapterSchema.deserialize, allOffsets, SortChapter(), ), sortLibraryAnime: reader.readObjectOrNull( - offsets[120], + offsets[128], SortLibraryMangaSchema.deserialize, allOffsets, ), sortLibraryManga: reader.readObjectOrNull( - offsets[121], + offsets[129], SortLibraryMangaSchema.deserialize, allOffsets, ), sortLibraryNovel: reader.readObjectOrNull( - offsets[122], + offsets[130], SortLibraryMangaSchema.deserialize, allOffsets, ), - startDatebackup: reader.readLongOrNull(offsets[123]), - themeIsDark: reader.readBoolOrNull(offsets[124]), - updateProgressAfterReading: reader.readBoolOrNull(offsets[125]), - updatedAt: reader.readLongOrNull(offsets[126]), - useLibass: reader.readBoolOrNull(offsets[127]), - useMpvConfig: reader.readBoolOrNull(offsets[128]), - usePageTapZones: reader.readBoolOrNull(offsets[129]), - useYUV420P: reader.readBoolOrNull(offsets[130]), - userAgent: reader.readStringOrNull(offsets[131]), - volumeBoostCap: reader.readLongOrNull(offsets[132]), + startDatebackup: reader.readLongOrNull(offsets[131]), + themeIsDark: reader.readBoolOrNull(offsets[132]), + updateProgressAfterReading: reader.readBoolOrNull(offsets[133]), + updatedAt: reader.readLongOrNull(offsets[134]), + useLibass: reader.readBoolOrNull(offsets[135]), + useMpvConfig: reader.readBoolOrNull(offsets[136]), + usePageTapZones: reader.readBoolOrNull(offsets[137]), + useYUV420P: reader.readBoolOrNull(offsets[138]), + userAgent: reader.readStringOrNull(offsets[139]), + volumeBoostCap: reader.readLongOrNull(offsets[140]), ); object.chapterFilterBookmarkedList = reader .readObjectList( @@ -1754,7 +1822,7 @@ Settings _settingsDeserialize( L10nLocaleSchema.deserialize, allOffsets, ); - object.novelGridSize = reader.readLongOrNull(offsets[98]); + object.novelGridSize = reader.readLongOrNull(offsets[99]); return object; } @@ -2070,12 +2138,14 @@ P _settingsDeserializeProp

( case 94: return (reader.readStringList(offset)) as P; case 95: + return (reader.readBoolOrNull(offset)) as P; + case 96: return (_SettingsnovelDisplayTypeValueEnumMap[reader.readByteOrNull( offset, )] ?? DisplayType.comfortableGrid) as P; - case 96: + case 97: return (reader.readObjectList( offset, RepoSchema.deserialize, @@ -2083,12 +2153,10 @@ P _settingsDeserializeProp

( Repo(), )) as P; - case 97: - return (reader.readLongOrNull(offset)) as P; case 98: return (reader.readLongOrNull(offset)) as P; case 99: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 100: return (reader.readBoolOrNull(offset)) as P; case 101: @@ -2100,16 +2168,32 @@ P _settingsDeserializeProp

( case 104: return (reader.readBoolOrNull(offset)) as P; case 105: + return (reader.readBoolOrNull(offset)) as P; + case 106: + return (reader.readDoubleOrNull(offset)) as P; + case 107: + return (reader.readLongOrNull(offset)) as P; + case 108: + return (reader.readStringOrNull(offset)) as P; + case 109: + return (reader.readStringOrNull(offset)) as P; + case 110: + return (reader.readBoolOrNull(offset)) as P; + case 111: + return (reader.readBoolOrNull(offset)) as P; + case 112: + return (reader.readBoolOrNull(offset)) as P; + case 113: return (_SettingsnovelTextAlignValueEnumMap[reader.readByteOrNull( offset, )] ?? NovelTextAlign.left) as P; - case 106: + case 114: return (reader.readBoolOrNull(offset)) as P; - case 107: + case 115: return (reader.readLongOrNull(offset)) as P; - case 108: + case 116: return (reader.readObjectList( offset, PersonalPageModeSchema.deserialize, @@ -2117,7 +2201,7 @@ P _settingsDeserializeProp

( PersonalPageMode(), )) as P; - case 109: + case 117: return (reader.readObjectList( offset, PersonalReaderModeSchema.deserialize, @@ -2125,32 +2209,32 @@ P _settingsDeserializeProp

( PersonalReaderMode(), )) as P; - case 110: + case 118: return (reader.readObjectOrNull( offset, PlayerSubtitleSettingsSchema.deserialize, allOffsets, )) as P; - case 111: + case 119: return (reader.readBoolOrNull(offset)) as P; - case 112: + case 120: return (reader.readLongOrNull(offset)) as P; - case 113: + case 121: return (reader.readBoolOrNull(offset)) as P; - case 114: + case 122: return (reader.readBoolOrNull(offset)) as P; - case 115: + case 123: return (reader.readBoolOrNull(offset)) as P; - case 116: + case 124: return (reader.readBoolOrNull(offset)) as P; - case 117: + case 125: return (_SettingsscaleTypeValueEnumMap[reader.readByteOrNull(offset)] ?? ScaleType.fitScreen) as P; - case 118: + case 126: return (reader.readBoolOrNull(offset)) as P; - case 119: + case 127: return (reader.readObjectList( offset, SortChapterSchema.deserialize, @@ -2158,46 +2242,46 @@ P _settingsDeserializeProp

( SortChapter(), )) as P; - case 120: - return (reader.readObjectOrNull( - offset, - SortLibraryMangaSchema.deserialize, - allOffsets, - )) - as P; - case 121: - return (reader.readObjectOrNull( - offset, - SortLibraryMangaSchema.deserialize, - allOffsets, - )) - as P; - case 122: - return (reader.readObjectOrNull( - offset, - SortLibraryMangaSchema.deserialize, - allOffsets, - )) - as P; - case 123: - return (reader.readLongOrNull(offset)) as P; - case 124: - return (reader.readBoolOrNull(offset)) as P; - case 125: - return (reader.readBoolOrNull(offset)) as P; - case 126: - return (reader.readLongOrNull(offset)) as P; - case 127: - return (reader.readBoolOrNull(offset)) as P; case 128: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readObjectOrNull( + offset, + SortLibraryMangaSchema.deserialize, + allOffsets, + )) + as P; case 129: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readObjectOrNull( + offset, + SortLibraryMangaSchema.deserialize, + allOffsets, + )) + as P; case 130: - return (reader.readBoolOrNull(offset)) as P; + return (reader.readObjectOrNull( + offset, + SortLibraryMangaSchema.deserialize, + allOffsets, + )) + as P; case 131: - return (reader.readStringOrNull(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 132: + return (reader.readBoolOrNull(offset)) as P; + case 133: + return (reader.readBoolOrNull(offset)) as P; + case 134: + return (reader.readLongOrNull(offset)) as P; + case 135: + return (reader.readBoolOrNull(offset)) as P; + case 136: + return (reader.readBoolOrNull(offset)) as P; + case 137: + return (reader.readBoolOrNull(offset)) as P; + case 138: + return (reader.readBoolOrNull(offset)) as P; + case 139: + return (reader.readStringOrNull(offset)) as P; + case 140: return (reader.readLongOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -9367,6 +9451,33 @@ extension SettingsQueryFilter }); } + QueryBuilder + novelAutoScrollIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'novelAutoScroll'), + ); + }); + } + + QueryBuilder + novelAutoScrollIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'novelAutoScroll'), + ); + }); + } + + QueryBuilder + novelAutoScrollEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'novelAutoScroll', value: value), + ); + }); + } + QueryBuilder novelDisplayTypeEqualTo(DisplayType value) { return QueryBuilder.apply(this, (query) { @@ -9849,6 +9960,587 @@ extension SettingsQueryFilter }); } + QueryBuilder + novelReaderLineHeightIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'novelReaderLineHeight'), + ); + }); + } + + QueryBuilder + novelReaderLineHeightIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'novelReaderLineHeight'), + ); + }); + } + + QueryBuilder + novelReaderLineHeightEqualTo( + double? value, { + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'novelReaderLineHeight', + value: value, + + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder + novelReaderLineHeightGreaterThan( + double? value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'novelReaderLineHeight', + value: value, + + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder + novelReaderLineHeightLessThan( + double? value, { + bool include = false, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'novelReaderLineHeight', + value: value, + + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder + novelReaderLineHeightBetween( + double? lower, + double? upper, { + bool includeLower = true, + bool includeUpper = true, + double epsilon = Query.epsilon, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'novelReaderLineHeight', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + + epsilon: epsilon, + ), + ); + }); + } + + QueryBuilder + novelReaderPaddingIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'novelReaderPadding'), + ); + }); + } + + QueryBuilder + novelReaderPaddingIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'novelReaderPadding'), + ); + }); + } + + QueryBuilder + novelReaderPaddingEqualTo(int? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'novelReaderPadding', value: value), + ); + }); + } + + QueryBuilder + novelReaderPaddingGreaterThan(int? value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'novelReaderPadding', + value: value, + ), + ); + }); + } + + QueryBuilder + novelReaderPaddingLessThan(int? value, {bool include = false}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'novelReaderPadding', + value: value, + ), + ); + }); + } + + QueryBuilder + novelReaderPaddingBetween( + int? lower, + int? upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'novelReaderPadding', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'novelReaderTextColor'), + ); + }); + } + + QueryBuilder + novelReaderTextColorIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'novelReaderTextColor'), + ); + }); + } + + QueryBuilder + novelReaderTextColorEqualTo(String? value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'novelReaderTextColor', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'novelReaderTextColor', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'novelReaderTextColor', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'novelReaderTextColor', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'novelReaderTextColor', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'novelReaderTextColor', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'novelReaderTextColor', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'novelReaderTextColor', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderTextColorIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'novelReaderTextColor', value: ''), + ); + }); + } + + QueryBuilder + novelReaderTextColorIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + property: r'novelReaderTextColor', + value: '', + ), + ); + }); + } + + QueryBuilder + novelReaderThemeIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'novelReaderTheme'), + ); + }); + } + + QueryBuilder + novelReaderThemeIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'novelReaderTheme'), + ); + }); + } + + QueryBuilder + novelReaderThemeEqualTo(String? value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'novelReaderTheme', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderThemeGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan( + include: include, + property: r'novelReaderTheme', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderThemeLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.lessThan( + include: include, + property: r'novelReaderTheme', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderThemeBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.between( + property: r'novelReaderTheme', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderThemeStartsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.startsWith( + property: r'novelReaderTheme', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderThemeEndsWith(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.endsWith( + property: r'novelReaderTheme', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderThemeContains(String value, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.contains( + property: r'novelReaderTheme', + value: value, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderThemeMatches(String pattern, {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.matches( + property: r'novelReaderTheme', + wildcard: pattern, + caseSensitive: caseSensitive, + ), + ); + }); + } + + QueryBuilder + novelReaderThemeIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'novelReaderTheme', value: ''), + ); + }); + } + + QueryBuilder + novelReaderThemeIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.greaterThan(property: r'novelReaderTheme', value: ''), + ); + }); + } + + QueryBuilder + novelRemoveExtraParagraphSpacingIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull( + property: r'novelRemoveExtraParagraphSpacing', + ), + ); + }); + } + + QueryBuilder + novelRemoveExtraParagraphSpacingIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull( + property: r'novelRemoveExtraParagraphSpacing', + ), + ); + }); + } + + QueryBuilder + novelRemoveExtraParagraphSpacingEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'novelRemoveExtraParagraphSpacing', + value: value, + ), + ); + }); + } + + QueryBuilder + novelShowScrollPercentageIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'novelShowScrollPercentage'), + ); + }); + } + + QueryBuilder + novelShowScrollPercentageIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'novelShowScrollPercentage'), + ); + }); + } + + QueryBuilder + novelShowScrollPercentageEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo( + property: r'novelShowScrollPercentage', + value: value, + ), + ); + }); + } + + QueryBuilder + novelTapToScrollIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNull(property: r'novelTapToScroll'), + ); + }); + } + + QueryBuilder + novelTapToScrollIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + const FilterCondition.isNotNull(property: r'novelTapToScroll'), + ); + }); + } + + QueryBuilder + novelTapToScrollEqualTo(bool? value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition( + FilterCondition.equalTo(property: r'novelTapToScroll', value: value), + ); + }); + } + QueryBuilder novelTextAlignEqualTo( NovelTextAlign value, ) { @@ -12439,6 +13131,18 @@ extension SettingsQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByNovelAutoScroll() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelAutoScroll', Sort.asc); + }); + } + + QueryBuilder sortByNovelAutoScrollDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelAutoScroll', Sort.desc); + }); + } + QueryBuilder sortByNovelDisplayType() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'novelDisplayType', Sort.asc); @@ -12565,6 +13269,97 @@ extension SettingsQuerySortBy on QueryBuilder { }); } + QueryBuilder sortByNovelReaderLineHeight() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderLineHeight', Sort.asc); + }); + } + + QueryBuilder + sortByNovelReaderLineHeightDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderLineHeight', Sort.desc); + }); + } + + QueryBuilder sortByNovelReaderPadding() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderPadding', Sort.asc); + }); + } + + QueryBuilder + sortByNovelReaderPaddingDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderPadding', Sort.desc); + }); + } + + QueryBuilder sortByNovelReaderTextColor() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderTextColor', Sort.asc); + }); + } + + QueryBuilder + sortByNovelReaderTextColorDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderTextColor', Sort.desc); + }); + } + + QueryBuilder sortByNovelReaderTheme() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderTheme', Sort.asc); + }); + } + + QueryBuilder sortByNovelReaderThemeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderTheme', Sort.desc); + }); + } + + QueryBuilder + sortByNovelRemoveExtraParagraphSpacing() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelRemoveExtraParagraphSpacing', Sort.asc); + }); + } + + QueryBuilder + sortByNovelRemoveExtraParagraphSpacingDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelRemoveExtraParagraphSpacing', Sort.desc); + }); + } + + QueryBuilder + sortByNovelShowScrollPercentage() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelShowScrollPercentage', Sort.asc); + }); + } + + QueryBuilder + sortByNovelShowScrollPercentageDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelShowScrollPercentage', Sort.desc); + }); + } + + QueryBuilder sortByNovelTapToScroll() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelTapToScroll', Sort.asc); + }); + } + + QueryBuilder sortByNovelTapToScrollDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelTapToScroll', Sort.desc); + }); + } + QueryBuilder sortByNovelTextAlign() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'novelTextAlign', Sort.asc); @@ -13856,6 +14651,18 @@ extension SettingsQuerySortThenBy }); } + QueryBuilder thenByNovelAutoScroll() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelAutoScroll', Sort.asc); + }); + } + + QueryBuilder thenByNovelAutoScrollDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelAutoScroll', Sort.desc); + }); + } + QueryBuilder thenByNovelDisplayType() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'novelDisplayType', Sort.asc); @@ -13982,6 +14789,97 @@ extension SettingsQuerySortThenBy }); } + QueryBuilder thenByNovelReaderLineHeight() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderLineHeight', Sort.asc); + }); + } + + QueryBuilder + thenByNovelReaderLineHeightDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderLineHeight', Sort.desc); + }); + } + + QueryBuilder thenByNovelReaderPadding() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderPadding', Sort.asc); + }); + } + + QueryBuilder + thenByNovelReaderPaddingDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderPadding', Sort.desc); + }); + } + + QueryBuilder thenByNovelReaderTextColor() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderTextColor', Sort.asc); + }); + } + + QueryBuilder + thenByNovelReaderTextColorDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderTextColor', Sort.desc); + }); + } + + QueryBuilder thenByNovelReaderTheme() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderTheme', Sort.asc); + }); + } + + QueryBuilder thenByNovelReaderThemeDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelReaderTheme', Sort.desc); + }); + } + + QueryBuilder + thenByNovelRemoveExtraParagraphSpacing() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelRemoveExtraParagraphSpacing', Sort.asc); + }); + } + + QueryBuilder + thenByNovelRemoveExtraParagraphSpacingDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelRemoveExtraParagraphSpacing', Sort.desc); + }); + } + + QueryBuilder + thenByNovelShowScrollPercentage() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelShowScrollPercentage', Sort.asc); + }); + } + + QueryBuilder + thenByNovelShowScrollPercentageDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelShowScrollPercentage', Sort.desc); + }); + } + + QueryBuilder thenByNovelTapToScroll() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelTapToScroll', Sort.asc); + }); + } + + QueryBuilder thenByNovelTapToScrollDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'novelTapToScroll', Sort.desc); + }); + } + QueryBuilder thenByNovelTextAlign() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'novelTextAlign', Sort.asc); @@ -14805,6 +15703,12 @@ extension SettingsQueryWhereDistinct }); } + QueryBuilder distinctByNovelAutoScroll() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'novelAutoScroll'); + }); + } + QueryBuilder distinctByNovelDisplayType() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'novelDisplayType'); @@ -14865,6 +15769,61 @@ extension SettingsQueryWhereDistinct }); } + QueryBuilder + distinctByNovelReaderLineHeight() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'novelReaderLineHeight'); + }); + } + + QueryBuilder distinctByNovelReaderPadding() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'novelReaderPadding'); + }); + } + + QueryBuilder distinctByNovelReaderTextColor({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy( + r'novelReaderTextColor', + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder distinctByNovelReaderTheme({ + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy( + r'novelReaderTheme', + caseSensitive: caseSensitive, + ); + }); + } + + QueryBuilder + distinctByNovelRemoveExtraParagraphSpacing() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'novelRemoveExtraParagraphSpacing'); + }); + } + + QueryBuilder + distinctByNovelShowScrollPercentage() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'novelShowScrollPercentage'); + }); + } + + QueryBuilder distinctByNovelTapToScroll() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'novelTapToScroll'); + }); + } + QueryBuilder distinctByNovelTextAlign() { return QueryBuilder.apply(this, (query) { return query.addDistinctBy(r'novelTextAlign'); @@ -15643,6 +16602,12 @@ extension SettingsQueryProperty }); } + QueryBuilder novelAutoScrollProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'novelAutoScroll'); + }); + } + QueryBuilder novelDisplayTypeProperty() { return QueryBuilder.apply(this, (query) { @@ -15711,6 +16676,52 @@ extension SettingsQueryProperty }); } + QueryBuilder + novelReaderLineHeightProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'novelReaderLineHeight'); + }); + } + + QueryBuilder novelReaderPaddingProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'novelReaderPadding'); + }); + } + + QueryBuilder + novelReaderTextColorProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'novelReaderTextColor'); + }); + } + + QueryBuilder novelReaderThemeProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'novelReaderTheme'); + }); + } + + QueryBuilder + novelRemoveExtraParagraphSpacingProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'novelRemoveExtraParagraphSpacing'); + }); + } + + QueryBuilder + novelShowScrollPercentageProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'novelShowScrollPercentage'); + }); + } + + QueryBuilder novelTapToScrollProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'novelTapToScroll'); + }); + } + QueryBuilder novelTextAlignProperty() { return QueryBuilder.apply(this, (query) { diff --git a/lib/modules/anime/providers/anime_player_controller_provider.g.dart b/lib/modules/anime/providers/anime_player_controller_provider.g.dart index 9307298d..cec7b204 100644 --- a/lib/modules/anime/providers/anime_player_controller_provider.g.dart +++ b/lib/modules/anime/providers/anime_player_controller_provider.g.dart @@ -59,7 +59,7 @@ final class AnimeStreamControllerProvider } String _$animeStreamControllerHash() => - r'486889b2b9f71759e4d9ff147b039436572cc01e'; + r'1bca3ada0f7919439500ce8c42fa39958c1c5a7b'; final class AnimeStreamControllerFamily extends $Family with diff --git a/lib/modules/library/providers/library_state_provider.g.dart b/lib/modules/library/providers/library_state_provider.g.dart index df1b8b73..02c46bfb 100644 --- a/lib/modules/library/providers/library_state_provider.g.dart +++ b/lib/modules/library/providers/library_state_provider.g.dart @@ -260,7 +260,7 @@ final class MangaFilterDownloadedStateProvider } String _$mangaFilterDownloadedStateHash() => - r'6d84bc7063be1734a0c267906a94e6b70e8b72fe'; + r'7ede8df99996399e368f5074dc1b3d4d7fa5e649'; final class MangaFilterDownloadedStateFamily extends $Family with @@ -379,7 +379,7 @@ final class MangaFilterUnreadStateProvider } String _$mangaFilterUnreadStateHash() => - r'bd96c9f42a40d0610788feda3bee5fb8662afe50'; + r'2bcea3aaccd923e415738d51511c0966a93a2900'; final class MangaFilterUnreadStateFamily extends $Family with diff --git a/lib/modules/main_view/providers/migration.g.dart b/lib/modules/main_view/providers/migration.g.dart index ab804af0..88113ea9 100644 --- a/lib/modules/main_view/providers/migration.g.dart +++ b/lib/modules/main_view/providers/migration.g.dart @@ -40,4 +40,4 @@ final class MigrationProvider } } -String _$migrationHash() => r'2a82120544e693a3162da887a3ca1b3066f3799f'; +String _$migrationHash() => r'43d62ddf79798d616ac7d11ce50a47551ef42c98'; diff --git a/lib/modules/manga/detail/manga_detail_view.dart b/lib/modules/manga/detail/manga_detail_view.dart index 96b2c55c..033e6be7 100644 --- a/lib/modules/manga/detail/manga_detail_view.dart +++ b/lib/modules/manga/detail/manga_detail_view.dart @@ -1937,11 +1937,7 @@ class _MangaDetailViewState extends ConsumerState children: [ Expanded(child: widget.action!), if (!isLocalArchive) Expanded(child: _smartUpdateDays()), - Expanded( - child: widget.itemType == ItemType.novel - ? SizedBox.shrink() - : _action(), - ), + if (widget.itemType != ItemType.novel) Expanded(child: _action()), if (!isLocalArchive) Expanded( child: SizedBox( diff --git a/lib/modules/manga/detail/manga_details_view.dart b/lib/modules/manga/detail/manga_details_view.dart index 7a2ab44d..2fa900b6 100644 --- a/lib/modules/manga/detail/manga_details_view.dart +++ b/lib/modules/manga/detail/manga_details_view.dart @@ -163,9 +163,16 @@ class _MangaDetailsViewState extends ConsumerState { titleDescription: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - widget.manga.author ?? "Unknown", - style: const TextStyle(fontWeight: FontWeight.w500), + Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + Icon(Icons.person_outline, size: 14), + const SizedBox(width: 4), + Text( + widget.manga.author ?? l10n.unknown, + style: const TextStyle(fontWeight: FontWeight.w500), + ), + ], ), Wrap( crossAxisAlignment: WrapCrossAlignment.center, diff --git a/lib/modules/manga/detail/providers/state_providers.g.dart b/lib/modules/manga/detail/providers/state_providers.g.dart index 39c9c95b..ba80a69c 100644 --- a/lib/modules/manga/detail/providers/state_providers.g.dart +++ b/lib/modules/manga/detail/providers/state_providers.g.dart @@ -701,7 +701,7 @@ final class ChapterSetIsBookmarkStateProvider } String _$chapterSetIsBookmarkStateHash() => - r'091d86aebaef46d2e9f35ae9f98c12c3e423f5b3'; + r'23b56105244d0aeed6ae9c27cee1897de8a306af'; final class ChapterSetIsBookmarkStateFamily extends $Family with @@ -800,7 +800,7 @@ final class ChapterSetIsReadStateProvider } String _$chapterSetIsReadStateHash() => - r'f5af852964964170905278d563fdb03eabed53b9'; + r'b75796ed2dd03bf3167258bcdf064817e8fa69c9'; final class ChapterSetIsReadStateFamily extends $Family with $ClassFamilyOverride { @@ -893,7 +893,7 @@ final class ChapterSetDownloadStateProvider } String _$chapterSetDownloadStateHash() => - r'2f35d274b76e28376b0089b2f6ee6d9d7ebcbeec'; + r'cb89abd653c018b762eb405634c7f8ca0ee8e99b'; final class ChapterSetDownloadStateFamily extends $Family with @@ -969,7 +969,7 @@ final class ChaptersListttStateProvider } String _$chaptersListttStateHash() => - r'5f1b0d2be32fcb904c12c5735f1340c8b33400a9'; + r'f45ebd9a5b1fd86b279e263813098564830c2536'; abstract class _$ChaptersListttState extends $Notifier> { List build(); @@ -1045,7 +1045,7 @@ final class ScanlatorsFilterStateProvider } String _$scanlatorsFilterStateHash() => - r'8da89864801cd7620029d28cfb3f9bee3c67cba8'; + r'f5220568e29e0c0efaac862fb0dce166f7be3172'; final class ScanlatorsFilterStateFamily extends $Family with diff --git a/lib/modules/manga/detail/providers/track_state_providers.dart b/lib/modules/manga/detail/providers/track_state_providers.dart index 6b00bc7b..47a1a7d1 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.dart @@ -1,4 +1,3 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; @@ -21,7 +20,7 @@ class TrackState extends _$TrackState { Track build({ Track? track, required ItemType? itemType, - required WidgetRef widgetRef, + required dynamic widgetRef, }) { return track!; } diff --git a/lib/modules/manga/detail/providers/track_state_providers.g.dart b/lib/modules/manga/detail/providers/track_state_providers.g.dart index 6ad8e3fc..50656098 100644 --- a/lib/modules/manga/detail/providers/track_state_providers.g.dart +++ b/lib/modules/manga/detail/providers/track_state_providers.g.dart @@ -15,7 +15,7 @@ const trackStateProvider = TrackStateFamily._(); final class TrackStateProvider extends $NotifierProvider { const TrackStateProvider._({ required TrackStateFamily super.from, - required ({Track? track, ItemType? itemType, WidgetRef widgetRef}) + required ({Track? track, ItemType? itemType, dynamic widgetRef}) super.argument, }) : super( retry: null, @@ -58,7 +58,7 @@ final class TrackStateProvider extends $NotifierProvider { } } -String _$trackStateHash() => r'cd19c5662338c7f0e508cf2f99e89c21f146d664'; +String _$trackStateHash() => r'c3e386652db112f64ce5605afeb5e7a49afbc397'; final class TrackStateFamily extends $Family with @@ -67,7 +67,7 @@ final class TrackStateFamily extends $Family Track, Track, Track, - ({Track? track, ItemType? itemType, WidgetRef widgetRef}) + ({Track? track, ItemType? itemType, dynamic widgetRef}) > { const TrackStateFamily._() : super( @@ -81,7 +81,7 @@ final class TrackStateFamily extends $Family TrackStateProvider call({ Track? track, required ItemType? itemType, - required WidgetRef widgetRef, + required dynamic widgetRef, }) => TrackStateProvider._( argument: (track: track, itemType: itemType, widgetRef: widgetRef), from: this, @@ -93,15 +93,15 @@ final class TrackStateFamily extends $Family abstract class _$TrackState extends $Notifier { late final _$args = - ref.$arg as ({Track? track, ItemType? itemType, WidgetRef widgetRef}); + ref.$arg as ({Track? track, ItemType? itemType, dynamic widgetRef}); Track? get track => _$args.track; ItemType? get itemType => _$args.itemType; - WidgetRef get widgetRef => _$args.widgetRef; + dynamic get widgetRef => _$args.widgetRef; Track build({ Track? track, required ItemType? itemType, - required WidgetRef widgetRef, + required dynamic widgetRef, }); @$mustCallSuper @override diff --git a/lib/modules/manga/detail/widgets/chapter_sort_list_tile_widget.dart b/lib/modules/manga/detail/widgets/chapter_sort_list_tile_widget.dart index 585a1a23..6437a372 100644 --- a/lib/modules/manga/detail/widgets/chapter_sort_list_tile_widget.dart +++ b/lib/modules/manga/detail/widgets/chapter_sort_list_tile_widget.dart @@ -19,7 +19,7 @@ class ListTileChapterSort extends StatelessWidget { iconColor: Theme.of(context).primaryColor, dense: true, leading: Icon( - reverse ? Icons.arrow_downward_sharp : Icons.arrow_upward_sharp, + !reverse ? Icons.arrow_downward_sharp : Icons.arrow_upward_sharp, color: showLeading ? Theme.of(context).primaryColor : Colors.transparent, diff --git a/lib/modules/manga/detail/widgets/expandable_text.dart b/lib/modules/manga/detail/widgets/expandable_text.dart new file mode 100644 index 00000000..5a04f56d --- /dev/null +++ b/lib/modules/manga/detail/widgets/expandable_text.dart @@ -0,0 +1,555 @@ +import 'dart:math'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; + +typedef StringCallback = void Function(String value); + +class ExpandableText extends StatefulWidget { + const ExpandableText( + this.text, { + super.key, + required this.expandText, + this.collapseText, + this.expanded = false, + this.onExpandedChanged, + this.onLinkTap, + this.linkColor, + this.linkEllipsis = true, + this.linkStyle, + this.prefixText, + this.prefixStyle, + this.onPrefixTap, + this.urlStyle, + this.onUrlTap, + this.hashtagStyle, + this.onHashtagTap, + this.mentionStyle, + this.onMentionTap, + this.expandOnTextTap = false, + this.collapseOnTextTap = false, + this.style, + this.textDirection, + this.textAlign, + this.textScaler, + this.maxLines = 2, + this.animation = false, + this.animationDuration, + this.animationCurve, + this.semanticsLabel, + this.showGradientOverlay = false, + this.gradientOverlayHeight = 30.0, + this.showExpandCollapseIcon = false, + this.expandIcon, + this.collapseIcon, + }) : assert(maxLines > 0); + + final String text; + final String expandText; + final String? collapseText; + final bool expanded; + final ValueChanged? onExpandedChanged; + final VoidCallback? onLinkTap; + final Color? linkColor; + final bool linkEllipsis; + final TextStyle? linkStyle; + final String? prefixText; + final TextStyle? prefixStyle; + final VoidCallback? onPrefixTap; + final TextStyle? urlStyle; + final StringCallback? onUrlTap; + final TextStyle? hashtagStyle; + final StringCallback? onHashtagTap; + final TextStyle? mentionStyle; + final StringCallback? onMentionTap; + final bool expandOnTextTap; + final bool collapseOnTextTap; + final TextStyle? style; + final TextDirection? textDirection; + final TextAlign? textAlign; + final TextScaler? textScaler; + final int maxLines; + final bool animation; + final Duration? animationDuration; + final Curve? animationCurve; + final String? semanticsLabel; + final bool showGradientOverlay; + final double gradientOverlayHeight; + final bool showExpandCollapseIcon; + final IconData? expandIcon; + final IconData? collapseIcon; + + @override + ExpandableTextState createState() => ExpandableTextState(); +} + +class ExpandableTextState extends State + with TickerProviderStateMixin { + bool _expanded = false; + late TapGestureRecognizer _linkTapGestureRecognizer; + late TapGestureRecognizer _prefixTapGestureRecognizer; + + List _textSegments = []; + final List _textSegmentsTapGestureRecognizers = []; + + @override + void initState() { + super.initState(); + + _expanded = widget.expanded; + _linkTapGestureRecognizer = TapGestureRecognizer()..onTap = _linkTapped; + _prefixTapGestureRecognizer = TapGestureRecognizer()..onTap = _prefixTapped; + + _updateText(); + } + + @override + void didUpdateWidget(ExpandableText oldWidget) { + super.didUpdateWidget(oldWidget); + + if (oldWidget.text != widget.text || + oldWidget.onUrlTap != widget.onUrlTap || + oldWidget.onHashtagTap != widget.onHashtagTap || + oldWidget.onMentionTap != widget.onMentionTap) { + _updateText(); + } + } + + @override + void dispose() { + _linkTapGestureRecognizer.dispose(); + _prefixTapGestureRecognizer.dispose(); + for (var recognizer in _textSegmentsTapGestureRecognizers) { + recognizer.dispose(); + } + super.dispose(); + } + + void _linkTapped() { + if (widget.onLinkTap != null) { + widget.onLinkTap!(); + return; + } + + final toggledExpanded = !_expanded; + + setState(() => _expanded = toggledExpanded); + + widget.onExpandedChanged?.call(toggledExpanded); + } + + void _prefixTapped() { + widget.onPrefixTap?.call(); + } + + @override + Widget build(BuildContext context) { + final DefaultTextStyle defaultTextStyle = DefaultTextStyle.of(context); + var effectiveTextStyle = widget.style; + if (widget.style == null || widget.style!.inherit) { + effectiveTextStyle = defaultTextStyle.style.merge(widget.style); + } + + final linkText = + (_expanded ? widget.collapseText : widget.expandText) ?? ''; + final linkColor = + widget.linkColor ?? + widget.linkStyle?.color ?? + Theme.of(context).colorScheme.secondary; + final linkTextStyle = effectiveTextStyle! + .merge(widget.linkStyle) + .copyWith(color: linkColor); + + final prefixText = + widget.prefixText != null && widget.prefixText!.isNotEmpty + ? '${widget.prefixText} ' + : ''; + + final link = TextSpan( + children: [ + if (!_expanded) + TextSpan( + text: '\u2026 ', + style: widget.linkEllipsis ? linkTextStyle : effectiveTextStyle, + recognizer: widget.linkEllipsis ? _linkTapGestureRecognizer : null, + ), + if (linkText.isNotEmpty) + TextSpan( + style: effectiveTextStyle, + children: [ + if (_expanded) const TextSpan(text: ' '), + TextSpan( + text: linkText, + style: linkTextStyle, + recognizer: _linkTapGestureRecognizer, + ), + ], + ), + ], + ); + + final prefix = TextSpan( + text: prefixText, + style: effectiveTextStyle.merge(widget.prefixStyle), + recognizer: _prefixTapGestureRecognizer, + ); + + final text = _textSegments.isNotEmpty + ? TextSpan( + children: _buildTextSpans(_textSegments, effectiveTextStyle, null), + ) + : TextSpan(text: widget.text); + + final content = TextSpan( + children: [prefix, text], + style: effectiveTextStyle, + ); + + Widget result = LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + assert(constraints.hasBoundedWidth); + final double maxWidth = constraints.maxWidth; + + final textAlign = + widget.textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start; + final textDirection = + widget.textDirection ?? Directionality.of(context); + final textScaler = + widget.textScaler ?? MediaQuery.textScalerOf(context); + final locale = Localizations.maybeLocaleOf(context); + + TextPainter textPainter = TextPainter( + text: link, + textAlign: textAlign, + textDirection: textDirection, + textScaler: textScaler, + maxLines: widget.maxLines, + locale: locale, + ); + textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth); + final linkSize = textPainter.size; + + textPainter.text = content; + textPainter.layout(minWidth: constraints.minWidth, maxWidth: maxWidth); + final textSize = textPainter.size; + + final bool hasExceededMaxLines = textPainter.didExceedMaxLines; + + TextSpan textSpan; + if (hasExceededMaxLines) { + final position = textPainter.getPositionForOffset( + Offset(textSize.width - linkSize.width, textSize.height), + ); + final endOffset = + (textPainter.getOffsetBefore(position.offset) ?? 0) - + prefixText.length; + + final recognizer = + (_expanded ? widget.collapseOnTextTap : widget.expandOnTextTap) + ? _linkTapGestureRecognizer + : null; + + final text = _textSegments.isNotEmpty + ? TextSpan( + children: _buildTextSpans( + _expanded + ? _textSegments + : parseText( + widget.text.substring(0, max(endOffset, 0)), + ), + effectiveTextStyle!, + recognizer, + ), + ) + : TextSpan( + text: _expanded + ? widget.text + : widget.text.substring(0, max(endOffset, 0)), + recognizer: recognizer, + ); + + textSpan = TextSpan( + style: effectiveTextStyle, + children: [prefix, text, link], + ); + } else { + textSpan = content; + } + + final selectableText = SelectableText.rich( + textSpan, + textDirection: textDirection, + textAlign: textAlign, + textScaler: textScaler, + ); + + Widget textWidget = selectableText; + + if (widget.animation) { + textWidget = AnimatedSize( + duration: + widget.animationDuration ?? const Duration(milliseconds: 200), + curve: widget.animationCurve ?? Curves.fastLinearToSlowEaseIn, + alignment: Alignment.topLeft, + child: textWidget, + ); + } + + // Wrap with Stack to add gradient overlay and icons + if (widget.showGradientOverlay || widget.showExpandCollapseIcon) { + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Stack( + children: [ + textWidget, + // Gradient overlay when collapsed and text exceeds max lines + if (widget.showGradientOverlay && + !_expanded && + hasExceededMaxLines) + Positioned( + bottom: 0, + left: 0, + right: 0, + child: GestureDetector( + onTap: _linkTapped, + child: Container( + height: widget.gradientOverlayHeight, + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Theme.of(context).scaffoldBackgroundColor + .withValues(alpha: 0.2), + Theme.of(context).scaffoldBackgroundColor, + ], + stops: const [0, 0.9], + ), + ), + child: Align( + alignment: Alignment.bottomCenter, + child: Icon( + widget.expandIcon ?? + Icons.keyboard_arrow_down_sharp, + color: Theme.of(context).iconTheme.color, + ), + ), + ), + ), + ), + ], + ), + // Collapse icon when expanded + if (widget.showExpandCollapseIcon && + _expanded && + hasExceededMaxLines) + GestureDetector( + onTap: _linkTapped, + child: SizedBox( + height: 20, + child: Icon( + widget.collapseIcon ?? Icons.keyboard_arrow_up_sharp, + color: Theme.of(context).iconTheme.color, + ), + ), + ), + ], + ); + } + + return textWidget; + }, + ); + + if (widget.semanticsLabel != null) { + result = Semantics( + textDirection: widget.textDirection, + label: widget.semanticsLabel, + child: ExcludeSemantics(child: result), + ); + } + + return result; + } + + void _updateText() { + for (var recognizer in _textSegmentsTapGestureRecognizers) { + recognizer.dispose(); + } + _textSegmentsTapGestureRecognizers.clear(); + + if (widget.onUrlTap == null && + widget.onHashtagTap == null && + widget.onMentionTap == null) { + _textSegments.clear(); + return; + } + + _textSegments = parseText(widget.text); + + for (var element in _textSegments) { + if (element.isUrl && widget.onUrlTap != null) { + final recognizer = TapGestureRecognizer() + ..onTap = () { + widget.onUrlTap!(element.name!); + }; + + _textSegmentsTapGestureRecognizers.add(recognizer); + } else if (element.isHashtag && widget.onHashtagTap != null) { + final recognizer = TapGestureRecognizer() + ..onTap = () { + widget.onHashtagTap!(element.name!); + }; + + _textSegmentsTapGestureRecognizers.add(recognizer); + } else if (element.isMention && widget.onMentionTap != null) { + final recognizer = TapGestureRecognizer() + ..onTap = () { + widget.onMentionTap!(element.name!); + }; + + _textSegmentsTapGestureRecognizers.add(recognizer); + } + } + } + + List _buildTextSpans( + List segments, + TextStyle textStyle, + TapGestureRecognizer? textTapRecognizer, + ) { + final spans = []; + + var index = 0; + for (var segment in segments) { + TextStyle? style; + TapGestureRecognizer? recognizer; + + if (segment.isUrl && widget.onUrlTap != null) { + style = textStyle.merge(widget.urlStyle); + recognizer = _textSegmentsTapGestureRecognizers[index++]; + } else if (segment.isMention && widget.onMentionTap != null) { + style = textStyle.merge(widget.mentionStyle); + recognizer = _textSegmentsTapGestureRecognizers[index++]; + } else if (segment.isHashtag && widget.onHashtagTap != null) { + style = textStyle.merge(widget.hashtagStyle); + recognizer = _textSegmentsTapGestureRecognizers[index++]; + } + + final span = TextSpan( + text: segment.text, + style: style, + recognizer: recognizer ?? textTapRecognizer, + ); + + spans.add(span); + } + + return spans; + } +} + +class TextSegment { + String text; + + final String? name; + final bool isHashtag; + final bool isMention; + final bool isUrl; + + bool get isText => !isHashtag && !isMention && !isUrl; + + TextSegment( + this.text, [ + this.name, + this.isHashtag = false, + this.isMention = false, + this.isUrl = false, + ]); + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TextSegment && + runtimeType == other.runtimeType && + text == other.text && + name == other.name && + isHashtag == other.isHashtag && + isMention == other.isMention && + isUrl == other.isUrl; + + @override + int get hashCode => + text.hashCode ^ + name.hashCode ^ + isHashtag.hashCode ^ + isMention.hashCode ^ + isUrl.hashCode; +} + +/// Split the string into multiple instances of [TextSegment] for mentions, hashtags, URLs and regular text. +/// +/// Mentions are all words that start with @, e.g. @mention. +/// Hashtags are all words that start with #, e.g. #hashtag. +List parseText(String? text) { + final segments = []; + + if (text == null || text.isEmpty) { + return segments; + } + + // parse urls and words starting with @ (mention) or # (hashtag) + RegExp exp = RegExp( + r'(?(#|@)([\p{Alphabetic}\p{Mark}\p{Decimal_Number}\p{Connector_Punctuation}\p{Join_Control}]+)|(?(?:(?:https?|ftp):\/\/)?[-a-z0-9@:%._\+~#=]{1,256}\.[a-z0-9]{1,6}(\/[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)?))', + unicode: true, + ); + final matches = exp.allMatches(text); + + var start = 0; + for (var match in matches) { + // text before the keyword + if (match.start > start) { + if (segments.isNotEmpty && segments.last.isText) { + segments.last.text += text.substring(start, match.start); + } else { + segments.add(TextSegment(text.substring(start, match.start))); + } + start = match.start; + } + + final url = match.namedGroup('url'); + final keyword = match.namedGroup('keyword'); + + if (url != null) { + segments.add(TextSegment(url, url, false, false, true)); + } else if (keyword != null) { + final isWord = + match.start == 0 || + [' ', '\n'].contains(text.substring(match.start - 1, start)); + if (!isWord) { + continue; + } + + final isHashtag = keyword.startsWith('#'); + final isMention = keyword.startsWith('@'); + + segments.add( + TextSegment(keyword, keyword.substring(1), isHashtag, isMention), + ); + } + + start = match.end; + } + + // text after the last keyword or the whole text if it does not contain any keywords + if (start < text.length) { + if (segments.isNotEmpty && segments.last.isText) { + segments.last.text += text.substring(start); + } else { + segments.add(TextSegment(text.substring(start))); + } + } + + return segments; +} diff --git a/lib/modules/manga/detail/widgets/readmore.dart b/lib/modules/manga/detail/widgets/readmore.dart index 786b9672..4dd12172 100644 --- a/lib/modules/manga/detail/widgets/readmore.dart +++ b/lib/modules/manga/detail/widgets/readmore.dart @@ -1,14 +1,13 @@ -import 'package:expandable_text/expandable_text.dart'; import 'package:flutter/material.dart'; +import 'package:mangayomi/modules/manga/detail/widgets/expandable_text.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; -import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; class ReadMoreWidget extends StatefulWidget { const ReadMoreWidget({ super.key, required this.text, required this.onChanged, - this.initExpanded = true, + this.initExpanded = false, }); final Function(bool) onChanged; final String text; @@ -29,63 +28,29 @@ class ReadMoreWidgetState extends State mainAxisAlignment: MainAxisAlignment.center, children: [Text(l10n.no_description)], ) - : Column( - children: [ - Stack( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: ExpandableText( - animationDuration: const Duration(milliseconds: 500), - onExpandedChanged: (value) { - setState(() => expanded = value); - widget.onChanged(value); - }, - expandOnTextTap: true, - widget.text.trim(), - expandText: '', - maxLines: 3, - expanded: expanded, - linkColor: Theme.of(context).scaffoldBackgroundColor, - animation: true, - collapseOnTextTap: true, - prefixText: '', - ), - ), - if (!expanded) - Positioned( - bottom: 0, - right: 0, - left: 0, - child: Container( - width: context.width(1), - height: 30, - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Theme.of( - context, - ).scaffoldBackgroundColor.withValues(alpha: 0.2), - Theme.of(context).scaffoldBackgroundColor, - ], - stops: const [0, .9], - ), - ), - child: const Icon(Icons.keyboard_arrow_down_sharp), - ), - ), - ], - ), - if (expanded) - SizedBox( - width: context.width(1), - height: 20, - child: const Icon(Icons.keyboard_arrow_up_sharp), - ), - ], + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: ExpandableText( + widget.text.trim(), + expandText: '', + maxLines: 3, + expanded: expanded, + linkColor: Colors.transparent, + animation: true, + animationDuration: const Duration(milliseconds: 500), + expandOnTextTap: true, + collapseOnTextTap: true, + prefixText: '', + showGradientOverlay: true, + gradientOverlayHeight: 30, + showExpandCollapseIcon: true, + expandIcon: Icons.keyboard_arrow_down_sharp, + collapseIcon: Icons.keyboard_arrow_up_sharp, + onExpandedChanged: (value) { + setState(() => expanded = value); + widget.onChanged(value); + }, + ), ); } } diff --git a/lib/modules/manga/download/providers/download_provider.g.dart b/lib/modules/manga/download/providers/download_provider.g.dart index a7acc9a0..9917d970 100644 --- a/lib/modules/manga/download/providers/download_provider.g.dart +++ b/lib/modules/manga/download/providers/download_provider.g.dart @@ -136,7 +136,7 @@ final class DownloadChapterProvider } } -String _$downloadChapterHash() => r'5eb401736efdfb2990fda6e2d97160aaeb94aec1'; +String _$downloadChapterHash() => r'0eb04602246bb3c4dcc88397d97039dea3047cc6'; final class DownloadChapterFamily extends $Family with @@ -215,7 +215,7 @@ final class ProcessDownloadsProvider } } -String _$processDownloadsHash() => r'ef5107f9674f2175a7aa18b8e4fc4555f3b6b584'; +String _$processDownloadsHash() => r'caebad3bb681d7b38de4d09325310fc08bc1cd0a'; final class ProcessDownloadsFamily extends $Family with $FunctionalFamilyOverride, bool?> { diff --git a/lib/modules/manga/reader/image_view_webtoon.dart b/lib/modules/manga/reader/image_view_webtoon.dart new file mode 100644 index 00000000..efa9ece2 --- /dev/null +++ b/lib/modules/manga/reader/image_view_webtoon.dart @@ -0,0 +1,160 @@ +import 'package:flutter/material.dart'; +import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.dart'; +import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart'; +import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart'; +import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart'; +import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart'; +import 'package:photo_view/photo_view.dart'; +import 'package:photo_view/photo_view_gallery.dart'; +import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; +import 'package:mangayomi/models/settings.dart'; + +/// Main widget for virtual reading that replaces ScrollablePositionedList +class ImageViewWebtoon extends StatelessWidget { + final List pages; + final ItemScrollController itemScrollController; + final ScrollOffsetController scrollOffsetController; + final ItemPositionsListener itemPositionsListener; + final Axis scrollDirection; + final double minCacheExtent; + final int initialScrollIndex; + final ScrollPhysics physics; + final Function(UChapDataPreload data) onLongPressData; + final Function(bool) onFailedToLoadImage; + final BackgroundColor backgroundColor; + final bool isDoublePageMode; + final bool isHorizontalContinuous; + final ReaderMode readerMode; + final PhotoViewController photoViewController; + final PhotoViewScaleStateController photoViewScaleStateController; + final Alignment scalePosition; + final Function(ScaleEndDetails) onScaleEnd; + final Function(Offset) onDoubleTapDown; + final VoidCallback onDoubleTap; + + const ImageViewWebtoon({ + super.key, + required this.pages, + required this.itemScrollController, + required this.scrollOffsetController, + required this.itemPositionsListener, + required this.scrollDirection, + required this.minCacheExtent, + required this.initialScrollIndex, + required this.physics, + required this.onLongPressData, + required this.onFailedToLoadImage, + required this.backgroundColor, + required this.isDoublePageMode, + required this.isHorizontalContinuous, + required this.readerMode, + required this.photoViewController, + required this.photoViewScaleStateController, + required this.scalePosition, + required this.onScaleEnd, + required this.onDoubleTapDown, + required this.onDoubleTap, + }); + + @override + Widget build(BuildContext context) { + return PhotoViewGallery.builder( + itemCount: 1, + builder: (_, _) => PhotoViewGalleryPageOptions.customChild( + controller: photoViewController, + scaleStateController: photoViewScaleStateController, + basePosition: scalePosition, + onScaleEnd: (context, details, controllerValue) => onScaleEnd(details), + child: ScrollablePositionedList.separated( + scrollDirection: scrollDirection, + minCacheExtent: minCacheExtent, + initialScrollIndex: initialScrollIndex, + itemCount: pages.length, + physics: physics, + itemScrollController: itemScrollController, + scrollOffsetController: scrollOffsetController, + itemPositionsListener: itemPositionsListener, + itemBuilder: (context, index) => _buildItem(context, index), + separatorBuilder: _buildSeparator, + ), + ), + ); + } + + Widget _buildItem(BuildContext context, int index) { + if (isDoublePageMode && !isHorizontalContinuous) { + return _buildDoublePageItem(context, index); + } else { + return _buildSinglePageItem(context, index); + } + } + + Widget _buildSinglePageItem(BuildContext context, int index) { + final currentPage = pages[index]; + + if (currentPage.isTransitionPage) { + return GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition), + onDoubleTap: onDoubleTap, + child: TransitionViewVertical(data: currentPage), + ); + } + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition), + onDoubleTap: onDoubleTap, + child: ImageViewVertical( + data: currentPage, + failedToLoadImage: onFailedToLoadImage, + onLongPressData: onLongPressData, + isHorizontal: isHorizontalContinuous, + ), + ); + } + + Widget _buildDoublePageItem(BuildContext context, int index) { + final pageLength = pages.length; + if (index >= pageLength) { + return const SizedBox.shrink(); + } + + final int index1 = index * 2 - 1; + final int index2 = index1 + 1; + + final List datas = index == 0 + ? [pages[0], null] + : [ + index1 < pageLength ? pages[index1] : null, + index2 < pageLength ? pages[index2] : null, + ]; + + return GestureDetector( + behavior: HitTestBehavior.translucent, + onDoubleTapDown: (details) => onDoubleTapDown(details.globalPosition), + onDoubleTap: onDoubleTap, + child: DoubleColummVerticalView( + datas: datas, + backgroundColor: backgroundColor, + isFailedToLoadImage: onFailedToLoadImage, + onLongPressData: onLongPressData, + ), + ); + } + + Widget _buildSeparator(BuildContext context, int index) { + if (readerMode == ReaderMode.webtoon) { + return const SizedBox.shrink(); + } + + if (isHorizontalContinuous) { + return VerticalDivider( + color: getBackgroundColor(backgroundColor), + width: 6, + ); + } else { + return Divider(color: getBackgroundColor(backgroundColor), height: 6); + } + } +} diff --git a/lib/modules/manga/reader/providers/crop_borders_provider.dart b/lib/modules/manga/reader/providers/crop_borders_provider.dart index 1ed1a13c..8f89e568 100644 --- a/lib/modules/manga/reader/providers/crop_borders_provider.dart +++ b/lib/modules/manga/reader/providers/crop_borders_provider.dart @@ -23,12 +23,119 @@ Future cropBorders( return null; } - return await Isolate.run(() async { - await RustLib.init(); - final imageRes = processCropImage(image: imageBytes!); - RustLib.dispose(); - return imageRes; - }); + return imgCropIsolate.process(imageBytes); } return null; } + +class ImageCropIsolate { + bool _isRunning = false; + Isolate? _rustIsolate; + ReceivePort? _receivePort; + SendPort? _sendPort; + + Future start() async { + if (!_isRunning) { + try { + await _initRustIsolate(); + } catch (_) { + await stop(); + } + } + } + + Future _initRustIsolate() async { + _receivePort = ReceivePort(); + + _rustIsolate = await Isolate.spawn( + _rustIsolateEntryPoint, + _receivePort!.sendPort, + ); + + final completer = Completer(); + _receivePort!.listen((message) { + if (message is SendPort) { + completer.complete(message); + } + }); + + _sendPort = await completer.future; + _isRunning = true; + } + + static Future _rustIsolateEntryPoint(SendPort mainSendPort) async { + await RustLib.init(); + + final receivePort = ReceivePort(); + mainSendPort.send(receivePort.sendPort); + + await for (var message in receivePort) { + if (message is Map) { + try { + final imageBytes = message['imageBytes'] as Uint8List; + final responsePort = message['responsePort'] as SendPort; + + final croppedImage = processCropImage(image: imageBytes); + + responsePort.send({'success': true, 'data': croppedImage}); + } catch (e) { + final responsePort = message['responsePort'] as SendPort; + responsePort.send({'success': false, 'error': e.toString()}); + } + } else if (message == 'dispose') { + RustLib.dispose(); + break; + } + } + } + + Future process(Uint8List imageBytes) async { + await start(); + if (_sendPort == null) { + if (kDebugMode) { + print('Image crop isolate is not running'); + } + return null; + } + + final responsePort = ReceivePort(); + final completer = Completer(); + + responsePort.listen((response) { + responsePort.close(); + if (response is Map) { + if (response['success'] == true) { + completer.complete(response['data'] as Uint8List); + } else { + if (kDebugMode) { + print('Image cropping failed: ${response['error']}'); + } + completer.complete(Future.value(null)); + } + } + }); + + _sendPort!.send({ + 'imageBytes': imageBytes, + 'responsePort': responsePort.sendPort, + }); + + return completer.future; + } + + Future stop() async { + if (!_isRunning) { + return; + } + + _sendPort?.send('dispose'); + _rustIsolate?.kill(priority: Isolate.immediate); + _receivePort?.close(); + _sendPort = null; + _rustIsolate = null; + _receivePort = null; + _isRunning = false; + } +} + +final imgCropIsolate = ImageCropIsolate(); diff --git a/lib/modules/manga/reader/providers/crop_borders_provider.g.dart b/lib/modules/manga/reader/providers/crop_borders_provider.g.dart index 15980d5b..931dc164 100644 --- a/lib/modules/manga/reader/providers/crop_borders_provider.g.dart +++ b/lib/modules/manga/reader/providers/crop_borders_provider.g.dart @@ -68,7 +68,7 @@ final class CropBordersProvider } } -String _$cropBordersHash() => r'04b24357737d6cc75caa38feca77bb5d41f00aa6'; +String _$cropBordersHash() => r'f60987c3f38afd5e10263f3d6935e6007ff942f0'; final class CropBordersFamily extends $Family with diff --git a/lib/modules/manga/reader/providers/reader_controller_provider.g.dart b/lib/modules/manga/reader/providers/reader_controller_provider.g.dart index e35ca45e..231ac9b5 100644 --- a/lib/modules/manga/reader/providers/reader_controller_provider.g.dart +++ b/lib/modules/manga/reader/providers/reader_controller_provider.g.dart @@ -148,7 +148,7 @@ final class ReaderControllerProvider } } -String _$readerControllerHash() => r'25b13bbbbd961a5c3dbae3cc0ea58017d7bb5ce8'; +String _$readerControllerHash() => r'23eece0ca4e7b6cbf425488636ef942fe0d4c2bc'; final class ReaderControllerFamily extends $Family with diff --git a/lib/modules/manga/reader/reader_view.dart b/lib/modules/manga/reader/reader_view.dart index cd93ad8d..d7bcba21 100644 --- a/lib/modules/manga/reader/reader_view.dart +++ b/lib/modules/manga/reader/reader_view.dart @@ -40,10 +40,9 @@ import 'package:mangayomi/modules/manga/reader/widgets/circular_progress_indicat import 'package:mangayomi/modules/manga/reader/widgets/transition_view_paged.dart'; import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart'; import 'package:mangayomi/modules/manga/reader/providers/manga_reader_provider.dart'; -import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_reader_view.dart'; +import 'package:mangayomi/modules/manga/reader/image_view_webtoon.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart'; import 'package:photo_view/photo_view.dart'; -import 'package:photo_view/photo_view_gallery.dart'; import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; import 'package:share_plus/share_plus.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; @@ -243,11 +242,12 @@ class _MangaChapterPageGalleryState WidgetsBinding.instance.addObserver(this); } - final double _horizontalScaleValue = 1.0; + // final double _horizontalScaleValue = 1.0; + bool _isNextChapterPreloading = false; late int pagePreloadAmount = ref.read(pagePreloadAmountStateProvider); late bool _isBookmarked = _readerController.getChapterBookmarked(); - + bool _isLastPageTransition = false; final _currentReaderMode = StateProvider(() => null); PageMode? _pageMode; bool _isView = false; @@ -471,7 +471,7 @@ class _MangaChapterPageGalleryState if (cropBorders) { _processCropBorders(); } - final usePageTapZones = ref.watch(usePageTapZonesStateProvider); + final l10n = l10nLocalizations(context)!; return KeyboardListener( autofocus: true, @@ -564,97 +564,45 @@ class _MangaChapterPageGalleryState return Stack( children: [ _isVerticalOrHorizontalContinous() - ? PhotoViewGallery.builder( - itemCount: 1, - builder: (_, _) => - PhotoViewGalleryPageOptions.customChild( - controller: _photoViewController, - scaleStateController: - _photoViewScaleStateController, - basePosition: _scalePosition, - onScaleEnd: _onScaleEnd, - child: VirtualReaderView( - pages: _uChapDataPreload, - itemScrollController: _itemScrollController, - scrollOffsetController: - _pageOffsetController, - itemPositionsListener: - _itemPositionsListener, - scrollDirection: isHorizontalContinuaous - ? Axis.horizontal - : Axis.vertical, - minCacheExtent: - pagePreloadAmount * context.height(1), - initialScrollIndex: _readerController - .getPageIndex(), - physics: const ClampingScrollPhysics(), - onLongPressData: (data) => - _onLongPressImageDialog(data, context), - onFailedToLoadImage: (value) { - // Handle failed image loading - if (_failedToLoadImage.value != value && - context.mounted) { - _failedToLoadImage.value = value; - } - }, - backgroundColor: backgroundColor, - isDoublePageMode: - _pageMode == PageMode.doublePage && - !isHorizontalContinuaous, - isHorizontalContinuous: - isHorizontalContinuaous, - readerMode: ref.watch(_currentReaderMode)!, - photoViewController: _photoViewController, - photoViewScaleStateController: - _photoViewScaleStateController, - scalePosition: _scalePosition, - onScaleEnd: (details) => _onScaleEnd( - context, - details, - _photoViewController.value, - ), - onDoubleTapDown: (offset) => - _toggleScale(offset), - onDoubleTap: () {}, - // Chapter transition callbacks - onChapterChanged: (newChapter) { - // Update the current chapter when a chapter change is detected - if (newChapter.id != chapter.id) { - if (mounted) { - setState(() { - _readerController = ref.read( - readerControllerProvider( - chapter: newChapter, - ).notifier, - ); - chapter = newChapter; - _isBookmarked = _readerController - .getChapterBookmarked(); - }); - } - } - }, - onReachedLastPage: (lastPageIndex) { - try { - ref - .watch( - getChapterPagesProvider( - chapter: _readerController - .getNextChapter(), - ).future, - ) - .then( - (value) => _preloadNextChapter( - value, - chapter, - ), - ); - } on RangeError { - _addLastPageTransition(chapter); - } - }, - ), - ), + ? ImageViewWebtoon( + pages: _uChapDataPreload, + itemScrollController: _itemScrollController, + scrollOffsetController: _pageOffsetController, + itemPositionsListener: _itemPositionsListener, + scrollDirection: isHorizontalContinuaous + ? Axis.horizontal + : Axis.vertical, + minCacheExtent: + pagePreloadAmount * context.height(1), + initialScrollIndex: _readerController + .getPageIndex(), + physics: const ClampingScrollPhysics(), + onLongPressData: (data) => + _onLongPressImageDialog(data, context), + onFailedToLoadImage: (value) { + // Handle failed image loading + if (_failedToLoadImage.value != value && + context.mounted) { + _failedToLoadImage.value = value; + } + }, + backgroundColor: backgroundColor, + isDoublePageMode: + _pageMode == PageMode.doublePage && + !isHorizontalContinuaous, + isHorizontalContinuous: isHorizontalContinuaous, + readerMode: ref.watch(_currentReaderMode)!, + photoViewController: _photoViewController, + photoViewScaleStateController: + _photoViewScaleStateController, + scalePosition: _scalePosition, + onScaleEnd: (details) => _onScaleEnd( + context, + details, + _photoViewController.value, + ), + onDoubleTapDown: (offset) => _toggleScale(offset), + onDoubleTap: () {}, ) : Material( color: getBackgroundColor(backgroundColor), @@ -941,8 +889,28 @@ class _MangaChapterPageGalleryState onPageChanged: _onPageChanged, ), ), - _gestureRightLeft(failedToLoadImage, usePageTapZones), - _gestureTopBottom(failedToLoadImage, usePageTapZones), + Consumer( + builder: (context, ref, child) { + final usePageTapZones = ref.watch( + usePageTapZonesStateProvider, + ); + return _gestureRightLeft( + failedToLoadImage, + usePageTapZones, + ); + }, + ), + Consumer( + builder: (context, ref, child) { + final usePageTapZones = ref.watch( + usePageTapZonesStateProvider, + ); + return _gestureTopBottom( + failedToLoadImage, + usePageTapZones, + ); + }, + ), _appBar(), _bottomBar(), _showPage(), @@ -1020,16 +988,23 @@ class _MangaChapterPageGalleryState }); } } - if (itemPositions.last.index == pagesLength - 1) { + if ((itemPositions.last.index == pagesLength - 1) && + !_isLastPageTransition) { + if (_isNextChapterPreloading) return; try { + _isNextChapterPreloading = true; ref .watch( getChapterPagesProvider( chapter: _readerController.getNextChapter(), ).future, ) - .then((value) => _preloadNextChapter(value, chapter)); + .then((value) { + _preloadNextChapter(value, chapter); + _isNextChapterPreloading = false; + }); } on RangeError { + _isNextChapterPreloading = false; _addLastPageTransition(chapter); } } @@ -1042,6 +1017,7 @@ class _MangaChapterPageGalleryState } void _addLastPageTransition(Chapter chap) { + if (_isLastPageTransition) return; try { if (!mounted || (_uChapDataPreload.last.isLastChapter ?? false)) return; final currentLength = _uChapDataPreload.length; @@ -1056,6 +1032,7 @@ class _MangaChapterPageGalleryState if (mounted) { setState(() { _uChapDataPreload.add(transitionPage); + _isLastPageTransition = true; }); } } catch (_) {} @@ -1195,16 +1172,23 @@ class _MangaChapterPageGalleryState .setCurrentIndex(_uChapDataPreload[index].index!); } - if (_uChapDataPreload[index].pageIndex! == _uChapDataPreload.length - 1) { + if ((_uChapDataPreload[index].pageIndex! == _uChapDataPreload.length - 1) && + !_isLastPageTransition) { + if (_isNextChapterPreloading) return; try { + _isNextChapterPreloading = true; ref .watch( getChapterPagesProvider( chapter: _readerController.getNextChapter(), ).future, ) - .then((value) => _preloadNextChapter(value, chapter)); + .then((value) { + _preloadNextChapter(value, chapter); + _isNextChapterPreloading = false; + }); } on RangeError { + _isNextChapterPreloading = false; _addLastPageTransition(chapter); } } @@ -1413,6 +1397,7 @@ class _MangaChapterPageGalleryState } void _processCropBorders() async { + if (_cropBorderCheckList.length == _uChapDataPreload.length) return; for (var i = 0; i < _uChapDataPreload.length; i++) { if (!_cropBorderCheckList.contains(i)) { _cropBorderCheckList.add(i); @@ -1985,7 +1970,7 @@ class _MangaChapterPageGalleryState void _isViewFunction() { final fullScreenReader = ref.watch(fullScreenReaderStateProvider); - if (mounted) { + if (context.mounted) { setState(() { _isView = !_isView; }); diff --git a/lib/modules/manga/reader/virtual_scrolling/virtual_manga_list.dart b/lib/modules/manga/reader/virtual_scrolling/virtual_manga_list.dart deleted file mode 100644 index 43bfd4dd..00000000 --- a/lib/modules/manga/reader/virtual_scrolling/virtual_manga_list.dart +++ /dev/null @@ -1,343 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:mangayomi/models/settings.dart'; -import 'package:mangayomi/models/chapter.dart'; -import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart'; -import 'package:mangayomi/modules/manga/reader/image_view_vertical.dart'; -import 'package:mangayomi/modules/manga/reader/double_columm_view_vertical.dart'; -import 'package:mangayomi/modules/manga/reader/widgets/transition_view_vertical.dart'; -import 'package:mangayomi/modules/more/settings/reader/reader_screen.dart'; -import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; - -/// Widget for displaying manga pages in a virtual scrolling list -class VirtualMangaList extends ConsumerStatefulWidget { - final VirtualPageManager pageManager; - final ItemScrollController itemScrollController; - final ScrollOffsetController scrollOffsetController; - final ItemPositionsListener itemPositionsListener; - final Axis scrollDirection; - final double minCacheExtent; - final int initialScrollIndex; - final ScrollPhysics physics; - final Function(UChapDataPreload data) onLongPressData; - final Function(bool) onFailedToLoadImage; - final BackgroundColor backgroundColor; - final bool isDoublePageMode; - final bool isHorizontalContinuous; - final ReaderMode readerMode; - final Function(Offset) onDoubleTapDown; - final VoidCallback onDoubleTap; - final Function(Chapter chapter)? onChapterChanged; - final Function(int lastPageIndex)? onReachedLastPage; - final Function(int index)? onPageChanged; - - const VirtualMangaList({ - super.key, - required this.pageManager, - required this.itemScrollController, - required this.scrollOffsetController, - required this.itemPositionsListener, - required this.scrollDirection, - required this.minCacheExtent, - required this.initialScrollIndex, - required this.physics, - required this.onLongPressData, - required this.onFailedToLoadImage, - required this.backgroundColor, - required this.isDoublePageMode, - required this.isHorizontalContinuous, - required this.readerMode, - required this.onDoubleTapDown, - required this.onDoubleTap, - this.onChapterChanged, - this.onReachedLastPage, - this.onPageChanged, - }); - - @override - ConsumerState createState() => _VirtualMangaListState(); -} - -class _VirtualMangaListState extends ConsumerState { - Chapter? _currentChapter; - int? _currentIndex; - - @override - void initState() { - super.initState(); - - // Listen to item positions to update virtual page manager - widget.itemPositionsListener.itemPositions.addListener(_onPositionChanged); - - // Initialize current chapter - if (widget.pageManager.pageCount > 0) { - final firstPage = widget.pageManager.getOriginalPage( - widget.initialScrollIndex, - ); - _currentChapter = firstPage?.chapter; - } - } - - @override - void dispose() { - widget.itemPositionsListener.itemPositions.removeListener( - _onPositionChanged, - ); - super.dispose(); - } - - void _onPositionChanged() { - final positions = widget.itemPositionsListener.itemPositions.value; - if (positions.isNotEmpty) { - // Get the first visible item - final firstVisibleIndex = positions.first.index; - final lastVisibleIndex = positions.last.index; - - // Update virtual page manager - widget.pageManager.updateVisibleIndex(firstVisibleIndex); - - // Calculate actual page lengths considering page mode - int pagesLength = - widget.isDoublePageMode && !widget.isHorizontalContinuous - ? (widget.pageManager.pageCount / 2).ceil() + 1 - : widget.pageManager.pageCount; - - // Check if index is valid - if (firstVisibleIndex >= 0 && firstVisibleIndex < pagesLength) { - final currentPage = widget.pageManager.getOriginalPage( - firstVisibleIndex, - ); - - if (currentPage != null) { - // Check for chapter change - if (_currentChapter?.id != currentPage.chapter?.id && - currentPage.chapter != null) { - _currentChapter = currentPage.chapter; - widget.onChapterChanged?.call(currentPage.chapter!); - } - - // Update current index - if (_currentIndex != firstVisibleIndex) { - _currentIndex = firstVisibleIndex; - widget.onPageChanged?.call(firstVisibleIndex); - } - } - - // Check if reached last page to trigger next chapter preload - if (lastVisibleIndex >= pagesLength - 1) { - widget.onReachedLastPage?.call(lastVisibleIndex); - } - } - } - } - - @override - Widget build(BuildContext context) { - return ListenableBuilder( - listenable: widget.pageManager, - builder: (context, child) { - final itemCount = - widget.isDoublePageMode && !widget.isHorizontalContinuous - ? (widget.pageManager.pageCount / 2).ceil() + 1 - : widget.pageManager.pageCount; - - return ScrollablePositionedList.separated( - scrollDirection: widget.scrollDirection, - minCacheExtent: widget.minCacheExtent, - initialScrollIndex: widget.initialScrollIndex, - itemCount: itemCount, - physics: widget.physics, - itemScrollController: widget.itemScrollController, - scrollOffsetController: widget.scrollOffsetController, - itemPositionsListener: widget.itemPositionsListener, - itemBuilder: (context, index) => _buildItem(context, index), - separatorBuilder: _buildSeparator, - ); - }, - ); - } - - Widget _buildItem(BuildContext context, int index) { - if (widget.isDoublePageMode && !widget.isHorizontalContinuous) { - return _buildDoublePageItem(context, index); - } else { - return _buildSinglePageItem(context, index); - } - } - - Widget _buildSinglePageItem(BuildContext context, int index) { - final originalPage = widget.pageManager.getOriginalPage(index); - if (originalPage == null) { - return const SizedBox.shrink(); - } - - // Check if page should be loaded - final pageInfo = widget.pageManager.getPageInfo(index); - final shouldLoad = widget.pageManager.shouldPageBeLoaded(index); - - if (!shouldLoad && - (pageInfo?.loadState == PageLoadState.notLoaded || pageInfo == null)) { - // Return placeholder for unloaded pages - return _buildPlaceholder(context); - } - - if (originalPage.isTransitionPage) { - return GestureDetector( - behavior: HitTestBehavior.translucent, - onDoubleTapDown: (details) => - widget.onDoubleTapDown(details.globalPosition), - onDoubleTap: widget.onDoubleTap, - child: TransitionViewVertical(data: originalPage), - ); - } - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onDoubleTapDown: (details) => - widget.onDoubleTapDown(details.globalPosition), - onDoubleTap: widget.onDoubleTap, - child: ImageViewVertical( - data: originalPage, - failedToLoadImage: widget.onFailedToLoadImage, - onLongPressData: widget.onLongPressData, - isHorizontal: widget.isHorizontalContinuous, - ), - ); - } - - Widget _buildDoublePageItem(BuildContext context, int index) { - if (index >= widget.pageManager.pageCount) { - return const SizedBox.shrink(); - } - - final int index1 = index * 2 - 1; - final int index2 = index1 + 1; - - final List datas = index == 0 - ? [widget.pageManager.getOriginalPage(0), null] - : [ - index1 < widget.pageManager.pageCount - ? widget.pageManager.getOriginalPage(index1) - : null, - index2 < widget.pageManager.pageCount - ? widget.pageManager.getOriginalPage(index2) - : null, - ]; - - // Check if pages should be loaded - final shouldLoad1 = index1 >= 0 - ? widget.pageManager.shouldPageBeLoaded(index1) - : false; - final shouldLoad2 = index2 < widget.pageManager.pageCount - ? widget.pageManager.shouldPageBeLoaded(index2) - : false; - - if (!shouldLoad1 && !shouldLoad2) { - return _buildPlaceholder(context); - } - - return GestureDetector( - behavior: HitTestBehavior.translucent, - onDoubleTapDown: (details) => - widget.onDoubleTapDown(details.globalPosition), - onDoubleTap: widget.onDoubleTap, - child: DoubleColummVerticalView( - datas: datas, - backgroundColor: widget.backgroundColor, - isFailedToLoadImage: widget.onFailedToLoadImage, - onLongPressData: widget.onLongPressData, - ), - ); - } - - Widget _buildPlaceholder(BuildContext context) { - return Container( - height: context.height(0.8), - color: getBackgroundColor(widget.backgroundColor), - child: const Center(child: CircularProgressIndicator()), - ); - } - - Widget _buildSeparator(BuildContext context, int index) { - if (widget.readerMode == ReaderMode.webtoon) { - return const SizedBox.shrink(); - } - - if (widget.isHorizontalContinuous) { - return VerticalDivider( - color: getBackgroundColor(widget.backgroundColor), - width: 6, - ); - } else { - return Divider( - color: getBackgroundColor(widget.backgroundColor), - height: 6, - ); - } - } -} - -/// Debug widget to show virtual page manager statistics -class VirtualPageManagerDebugInfo extends ConsumerWidget { - final VirtualPageManager pageManager; - - const VirtualPageManagerDebugInfo({super.key, required this.pageManager}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return ListenableBuilder( - listenable: pageManager, - builder: (context, child) { - final stats = pageManager.getMemoryStats(); - - return Positioned( - top: 100, - right: 10, - child: Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.black54, - borderRadius: BorderRadius.circular(8), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Text( - 'Virtual Page Manager', - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 4), - Text( - 'Current: ${stats['currentIndex']}/${stats['totalPages']}', - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - Text( - 'Loaded: ${stats['loadedPages']}', - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - Text( - 'Cached: ${stats['cachedPages']}', - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - Text( - 'Errors: ${stats['errorPages']}', - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - Text( - 'Queue: ${stats['preloadQueueSize']}', - style: const TextStyle(color: Colors.white, fontSize: 12), - ), - ], - ), - ), - ); - }, - ); - } -} diff --git a/lib/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart b/lib/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart deleted file mode 100644 index 38ce27b5..00000000 --- a/lib/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart +++ /dev/null @@ -1,285 +0,0 @@ -import 'dart:async'; -import 'dart:math'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart'; - -/// Page loading states for virtual scrolling -enum PageLoadState { notLoaded, loading, loaded, error, cached } - -/// Virtual page information for tracking state -class VirtualPageInfo { - final int index; - final UChapDataPreload originalData; - PageLoadState loadState; - DateTime? lastAccessTime; - Object? error; - - VirtualPageInfo({ - required this.index, - required this.originalData, - this.loadState = PageLoadState.notLoaded, - this.lastAccessTime, - this.error, - }); - - bool get isVisible => - loadState == PageLoadState.loaded || loadState == PageLoadState.cached; - bool get needsLoading => loadState == PageLoadState.notLoaded; - bool get isLoading => loadState == PageLoadState.loading; - bool get hasError => loadState == PageLoadState.error; - - void markAccessed() { - lastAccessTime = DateTime.now(); - } - - Duration get timeSinceAccess { - if (lastAccessTime == null) return Duration.zero; - return DateTime.now().difference(lastAccessTime!); - } -} - -/// Configuration for virtual page manager -class VirtualPageConfig { - final int preloadDistance; - final int maxCachedPages; - final Duration cacheTimeout; - final bool enableMemoryOptimization; - - const VirtualPageConfig({ - this.preloadDistance = 3, - this.maxCachedPages = 10, - this.cacheTimeout = const Duration(minutes: 5), - this.enableMemoryOptimization = true, - }); -} - -/// Manages virtual page loading and memory optimization -class VirtualPageManager extends ChangeNotifier { - final List _originalPages; - final VirtualPageConfig config; - final Map _pageInfoMap = {}; - final Set _preloadQueue = {}; - - int _currentVisibleIndex = 0; - Timer? _cleanupTimer; - - VirtualPageManager({ - required List pages, - this.config = const VirtualPageConfig(), - }) : _originalPages = List.from(pages) { - _initializePages(); - _startCleanupTimer(); - } - - void _initializePages() { - for (int i = 0; i < _originalPages.length; i++) { - _pageInfoMap[i] = VirtualPageInfo( - index: i, - originalData: _originalPages[i], - ); - } - } - - void _startCleanupTimer() { - _cleanupTimer?.cancel(); - _cleanupTimer = Timer.periodic( - const Duration(seconds: 30), - (_) => _performMemoryCleanup(), - ); - } - - @override - void dispose() { - _cleanupTimer?.cancel(); - super.dispose(); - } - - /// Get page count - int get pageCount => _originalPages.length; - - /// Get current visible index - int get currentVisibleIndex => _currentVisibleIndex; - - /// Get page info for a specific index - VirtualPageInfo? getPageInfo(int index) { - if (index < 0 || index >= _originalPages.length) return null; - return _pageInfoMap[index]; - } - - /// Get original page data - UChapDataPreload? getOriginalPage(int index) { - if (index < 0 || index >= _originalPages.length) return null; - return _originalPages[index]; - } - - /// Update visible page index and trigger preloading - void updateVisibleIndex(int index) { - if (index == _currentVisibleIndex) return; - - _currentVisibleIndex = index.clamp(0, _originalPages.length - 1); - _pageInfoMap[_currentVisibleIndex]?.markAccessed(); - - _schedulePreloading(); - notifyListeners(); - } - - /// Check if a page should be visible/loaded - bool shouldPageBeLoaded(int index) { - final distance = (index - _currentVisibleIndex).abs(); - return distance <= config.preloadDistance; - } - - /// Get priority for a page (higher = more important) - int getPagePriority(int index) { - final distance = (index - _currentVisibleIndex).abs(); - if (distance == 0) return 1000; // Current page has highest priority - return max(0, 100 - distance * 10); - } - - /// Schedule preloading for nearby pages - void _schedulePreloading() { - _preloadQueue.clear(); - - // Add pages within preload distance - for (int i = 0; i < _originalPages.length; i++) { - if (shouldPageBeLoaded(i)) { - final pageInfo = _pageInfoMap[i]!; - if (pageInfo.needsLoading) { - _preloadQueue.add(i); - } - } - } - - // Process preload queue - _processPreloadQueue(); - } - - /// Process the preload queue - void _processPreloadQueue() { - final sortedQueue = _preloadQueue.toList() - ..sort((a, b) => getPagePriority(b).compareTo(getPagePriority(a))); - - for (final index in sortedQueue.take(3)) { - // Limit concurrent loading - _loadPage(index); - } - } - - /// Load a specific page - Future _loadPage(int index) async { - final pageInfo = _pageInfoMap[index]; - if (pageInfo == null || pageInfo.isLoading) return; - - pageInfo.loadState = PageLoadState.loading; - notifyListeners(); - - try { - // For now, we just mark as loaded since the actual image loading - // is handled by the ImageView widgets - await Future.delayed(const Duration(milliseconds: 10)); - - pageInfo.loadState = PageLoadState.loaded; - pageInfo.markAccessed(); - } catch (error) { - pageInfo.loadState = PageLoadState.error; - pageInfo.error = error; - } - - notifyListeners(); - } - - /// Perform memory cleanup - void _performMemoryCleanup() { - if (!config.enableMemoryOptimization) return; - - final pageEntries = _pageInfoMap.entries.toList(); - - // Sort by last access time and distance from current page - pageEntries.sort((a, b) { - final aDistance = (a.key - _currentVisibleIndex).abs(); - final bDistance = (b.key - _currentVisibleIndex).abs(); - final aTime = - a.value.lastAccessTime ?? DateTime.fromMillisecondsSinceEpoch(0); - final bTime = - b.value.lastAccessTime ?? DateTime.fromMillisecondsSinceEpoch(0); - - // First sort by distance, then by access time - final distanceComparison = aDistance.compareTo(bDistance); - return distanceComparison != 0 - ? distanceComparison - : aTime.compareTo(bTime); - }); - - int cachedCount = pageEntries.where((e) => e.value.isVisible).length; - - // Remove old cached pages if we exceed the limit - for (final entry in pageEntries) { - if (cachedCount <= config.maxCachedPages) break; - - final pageInfo = entry.value; - final distance = (entry.key - _currentVisibleIndex).abs(); - - // Don't unload pages within preload distance - if (distance <= config.preloadDistance) continue; - - // Don't unload recently accessed pages - if (pageInfo.timeSinceAccess < config.cacheTimeout) continue; - - if (pageInfo.isVisible) { - pageInfo.loadState = PageLoadState.notLoaded; - pageInfo.error = null; - cachedCount--; - } - } - - if (cachedCount != pageEntries.where((e) => e.value.isVisible).length) { - notifyListeners(); - } - } - - /// Force load a page immediately - Future forceLoadPage(int index) async { - await _loadPage(index); - } - - /// Get memory usage statistics - Map getMemoryStats() { - final loadedCount = _pageInfoMap.values - .where((p) => p.loadState == PageLoadState.loaded) - .length; - final cachedCount = _pageInfoMap.values - .where((p) => p.loadState == PageLoadState.cached) - .length; - final errorCount = _pageInfoMap.values.where((p) => p.hasError).length; - - return { - 'totalPages': _originalPages.length, - 'loadedPages': loadedCount, - 'cachedPages': cachedCount, - 'errorPages': errorCount, - 'currentIndex': _currentVisibleIndex, - 'preloadQueueSize': _preloadQueue.length, - }; - } - - /// Preload a range of pages - Future preloadRange(int startIndex, int endIndex) async { - for (int i = startIndex; i <= endIndex && i < _originalPages.length; i++) { - if (i >= 0) { - await _loadPage(i); - } - } - } - - /// Clear all cached pages - void clearCache() { - for (final pageInfo in _pageInfoMap.values) { - if (pageInfo.loadState != PageLoadState.loading) { - pageInfo.loadState = PageLoadState.notLoaded; - pageInfo.error = null; - } - } - notifyListeners(); - } -} diff --git a/lib/modules/manga/reader/virtual_scrolling/virtual_reader_view.dart b/lib/modules/manga/reader/virtual_scrolling/virtual_reader_view.dart deleted file mode 100644 index 5d5617f4..00000000 --- a/lib/modules/manga/reader/virtual_scrolling/virtual_reader_view.dart +++ /dev/null @@ -1,223 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart'; -import 'package:mangayomi/utils/riverpod.dart'; -import 'package:photo_view/photo_view.dart'; -import 'package:photo_view/photo_view_gallery.dart'; -import 'package:scrollable_positioned_list/scrollable_positioned_list.dart'; -import 'package:mangayomi/models/settings.dart'; -import 'package:mangayomi/models/chapter.dart'; -import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_page_manager.dart'; -import 'package:mangayomi/modules/manga/reader/virtual_scrolling/virtual_manga_list.dart'; - -/// Provides virtual page manager instances -final virtualPageManagerProvider = - Provider.family>((ref, pages) { - return VirtualPageManager(pages: pages); - }); - -/// Main widget for virtual reading that replaces ScrollablePositionedList -class VirtualReaderView extends ConsumerStatefulWidget { - final List pages; - final ItemScrollController itemScrollController; - final ScrollOffsetController scrollOffsetController; - final ItemPositionsListener itemPositionsListener; - final Axis scrollDirection; - final double minCacheExtent; - final int initialScrollIndex; - final ScrollPhysics physics; - final Function(UChapDataPreload data) onLongPressData; - final Function(bool) onFailedToLoadImage; - final BackgroundColor backgroundColor; - final bool isDoublePageMode; - final bool isHorizontalContinuous; - final ReaderMode readerMode; - final PhotoViewController photoViewController; - final PhotoViewScaleStateController photoViewScaleStateController; - final Alignment scalePosition; - final Function(ScaleEndDetails) onScaleEnd; - final Function(Offset) onDoubleTapDown; - final VoidCallback onDoubleTap; - final bool showDebugInfo; - // Callbacks pour gérer les transitions entre chapitres - final Function(Chapter chapter)? onChapterChanged; - final Function(int lastPageIndex)? onReachedLastPage; - - const VirtualReaderView({ - super.key, - required this.pages, - required this.itemScrollController, - required this.scrollOffsetController, - required this.itemPositionsListener, - required this.scrollDirection, - required this.minCacheExtent, - required this.initialScrollIndex, - required this.physics, - required this.onLongPressData, - required this.onFailedToLoadImage, - required this.backgroundColor, - required this.isDoublePageMode, - required this.isHorizontalContinuous, - required this.readerMode, - required this.photoViewController, - required this.photoViewScaleStateController, - required this.scalePosition, - required this.onScaleEnd, - required this.onDoubleTapDown, - required this.onDoubleTap, - this.showDebugInfo = false, - this.onChapterChanged, - this.onReachedLastPage, - }); - - @override - ConsumerState createState() => _VirtualReaderViewState(); -} - -class _VirtualReaderViewState extends ConsumerState { - late VirtualPageManager _pageManager; - - @override - void initState() { - super.initState(); - _pageManager = VirtualPageManager(pages: widget.pages); - - // Set initial visible index - _pageManager.updateVisibleIndex(widget.initialScrollIndex); - } - - @override - void didUpdateWidget(VirtualReaderView oldWidget) { - super.didUpdateWidget(oldWidget); - - // Update page manager if pages changed - if (widget.pages != oldWidget.pages) { - _pageManager.dispose(); - _pageManager = VirtualPageManager(pages: widget.pages); - _pageManager.updateVisibleIndex(widget.initialScrollIndex); - } - } - - @override - void dispose() { - _pageManager.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - if (_pageManager.pageCount < widget.pages.length) { - _pageManager = VirtualPageManager(pages: widget.pages); - } - return Stack( - children: [ - PhotoViewGallery.builder( - itemCount: 1, - builder: (_, _) => PhotoViewGalleryPageOptions.customChild( - controller: widget.photoViewController, - scaleStateController: widget.photoViewScaleStateController, - basePosition: widget.scalePosition, - onScaleEnd: (context, details, controllerValue) => - widget.onScaleEnd(details), - child: VirtualMangaList( - pageManager: _pageManager, - itemScrollController: widget.itemScrollController, - scrollOffsetController: widget.scrollOffsetController, - itemPositionsListener: widget.itemPositionsListener, - scrollDirection: widget.scrollDirection, - minCacheExtent: widget.minCacheExtent, - initialScrollIndex: widget.initialScrollIndex, - physics: widget.physics, - onLongPressData: widget.onLongPressData, - onFailedToLoadImage: widget.onFailedToLoadImage, - backgroundColor: widget.backgroundColor, - isDoublePageMode: widget.isDoublePageMode, - isHorizontalContinuous: widget.isHorizontalContinuous, - readerMode: widget.readerMode, - onDoubleTapDown: widget.onDoubleTapDown, - onDoubleTap: widget.onDoubleTap, - // Passer les callbacks pour les transitions entre chapitres - onChapterChanged: widget.onChapterChanged, - onReachedLastPage: widget.onReachedLastPage, - onPageChanged: (index) { - // Ici on peut ajouter une logique supplémentaire si nécessaire - // Par exemple, précaching d'images - _pageManager.updateVisibleIndex(index); - }, - ), - ), - ), - - // Debug info overlay - if (widget.showDebugInfo) - VirtualPageManagerDebugInfo(pageManager: _pageManager), - ], - ); - } -} - -/// Mixin to add virtual page manager capabilities to existing widgets -mixin VirtualPageManagerMixin - on ConsumerState { - VirtualPageManager? _virtualPageManager; - - VirtualPageManager get virtualPageManager { - _virtualPageManager ??= VirtualPageManager(pages: getPages()); - return _virtualPageManager!; - } - - /// Override this method to provide the pages list - List getPages(); - - /// Call this when pages change - void updateVirtualPages(List newPages) { - _virtualPageManager?.dispose(); - _virtualPageManager = VirtualPageManager(pages: newPages); - } - - /// Call this when the visible page changes - void updateVisiblePage(int index) { - virtualPageManager.updateVisibleIndex(index); - } - - @override - void dispose() { - _virtualPageManager?.dispose(); - super.dispose(); - } -} - -/// Configuration provider for virtual page manager -final virtualPageConfigProvider = Provider((ref) { - // Get user preferences for virtual scrolling configuration - final preloadAmount = ref.watch(readerPagePreloadAmountStateProvider); - - return VirtualPageConfig( - preloadDistance: preloadAmount, - maxCachedPages: preloadAmount * 3, - cacheTimeout: const Duration(minutes: 5), - enableMemoryOptimization: true, - ); -}); - -/// Provider for page preload amount (renamed to avoid conflicts) -final readerPagePreloadAmountStateProvider = StateProvider(() => 3); - -/// Extension to convert ReaderMode to virtual scrolling parameters -extension ReaderModeExtension on ReaderMode { - bool get isContinuous { - return this == ReaderMode.verticalContinuous || - this == ReaderMode.webtoon || - this == ReaderMode.horizontalContinuous; - } - - Axis get scrollDirection { - return this == ReaderMode.horizontalContinuous - ? Axis.horizontal - : Axis.vertical; - } - - bool get isHorizontalContinuous { - return this == ReaderMode.horizontalContinuous; - } -} diff --git a/lib/modules/more/data_and_storage/providers/backup.g.dart b/lib/modules/more/data_and_storage/providers/backup.g.dart index ce73a016..5162918a 100644 --- a/lib/modules/more/data_and_storage/providers/backup.g.dart +++ b/lib/modules/more/data_and_storage/providers/backup.g.dart @@ -65,7 +65,7 @@ final class DoBackUpProvider } } -String _$doBackUpHash() => r'd16d5b6e5ed2c20988fa2d49842524d70ac0ed0d'; +String _$doBackUpHash() => r'e0d28adf6b592e34f26fd6b566151f3691f1946a'; final class DoBackUpFamily extends $Family with diff --git a/lib/modules/more/settings/reader/providers/reader_state_provider.dart b/lib/modules/more/settings/reader/providers/reader_state_provider.dart index 9f912a8b..32fcf84c 100644 --- a/lib/modules/more/settings/reader/providers/reader_state_provider.dart +++ b/lib/modules/more/settings/reader/providers/reader_state_provider.dart @@ -302,3 +302,165 @@ class NovelTextAlignState extends _$NovelTextAlignState { ); } } + +@riverpod +class NovelReaderThemeState extends _$NovelReaderThemeState { + @override + String build() { + return isar.settings.getSync(227)!.novelReaderTheme ?? '#292832'; + } + + void set(String value) { + final settings = isar.settings.getSync(227); + state = value; + isar.writeTxnSync( + () => isar.settings.putSync( + settings! + ..novelReaderTheme = value + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + } +} + +@riverpod +class NovelReaderTextColorState extends _$NovelReaderTextColorState { + @override + String build() { + return isar.settings.getSync(227)!.novelReaderTextColor ?? '#CCCCCC'; + } + + void set(String value) { + final settings = isar.settings.getSync(227); + state = value; + isar.writeTxnSync( + () => isar.settings.putSync( + settings! + ..novelReaderTextColor = value + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + } +} + +@riverpod +class NovelReaderPaddingState extends _$NovelReaderPaddingState { + @override + int build() { + return isar.settings.getSync(227)!.novelReaderPadding ?? 16; + } + + void set(int value) { + final settings = isar.settings.getSync(227); + state = value; + isar.writeTxnSync( + () => isar.settings.putSync( + settings! + ..novelReaderPadding = value + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + } +} + +@riverpod +class NovelReaderLineHeightState extends _$NovelReaderLineHeightState { + @override + double build() { + return isar.settings.getSync(227)!.novelReaderLineHeight ?? 1.5; + } + + void set(double value) { + final settings = isar.settings.getSync(227); + state = value; + isar.writeTxnSync( + () => isar.settings.putSync( + settings! + ..novelReaderLineHeight = value + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + } +} + +@riverpod +class NovelShowScrollPercentageState extends _$NovelShowScrollPercentageState { + @override + bool build() { + return isar.settings.getSync(227)!.novelShowScrollPercentage ?? true; + } + + void set(bool value) { + final settings = isar.settings.getSync(227); + state = value; + isar.writeTxnSync( + () => isar.settings.putSync( + settings! + ..novelShowScrollPercentage = value + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + } +} + +@riverpod +class NovelAutoScrollState extends _$NovelAutoScrollState { + @override + bool build() { + return isar.settings.getSync(227)!.novelAutoScroll ?? false; + } + + void set(bool value) { + final settings = isar.settings.getSync(227); + state = value; + isar.writeTxnSync( + () => isar.settings.putSync( + settings! + ..novelAutoScroll = value + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + } +} + +@riverpod +class NovelRemoveExtraParagraphSpacingState + extends _$NovelRemoveExtraParagraphSpacingState { + @override + bool build() { + return isar.settings.getSync(227)!.novelRemoveExtraParagraphSpacing ?? + false; + } + + void set(bool value) { + final settings = isar.settings.getSync(227); + state = value; + isar.writeTxnSync( + () => isar.settings.putSync( + settings! + ..novelRemoveExtraParagraphSpacing = value + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + } +} + +@riverpod +class NovelTapToScrollState extends _$NovelTapToScrollState { + @override + bool build() { + return isar.settings.getSync(227)!.novelTapToScroll ?? false; + } + + void set(bool value) { + final settings = isar.settings.getSync(227); + state = value; + isar.writeTxnSync( + () => isar.settings.putSync( + settings! + ..novelTapToScroll = value + ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + ); + } +} diff --git a/lib/modules/more/settings/reader/providers/reader_state_provider.g.dart b/lib/modules/more/settings/reader/providers/reader_state_provider.g.dart index 857d1e85..38fe44d5 100644 --- a/lib/modules/more/settings/reader/providers/reader_state_provider.g.dart +++ b/lib/modules/more/settings/reader/providers/reader_state_provider.g.dart @@ -764,3 +764,440 @@ abstract class _$NovelTextAlignState extends $Notifier { element.handleValue(ref, created); } } + +@ProviderFor(NovelReaderThemeState) +const novelReaderThemeStateProvider = NovelReaderThemeStateProvider._(); + +final class NovelReaderThemeStateProvider + extends $NotifierProvider { + const NovelReaderThemeStateProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'novelReaderThemeStateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$novelReaderThemeStateHash(); + + @$internal + @override + NovelReaderThemeState create() => NovelReaderThemeState(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$novelReaderThemeStateHash() => + r'3149f8ea16353f770b57cce9f27f3e63d062ee7b'; + +abstract class _$NovelReaderThemeState extends $Notifier { + String build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + String, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +@ProviderFor(NovelReaderTextColorState) +const novelReaderTextColorStateProvider = NovelReaderTextColorStateProvider._(); + +final class NovelReaderTextColorStateProvider + extends $NotifierProvider { + const NovelReaderTextColorStateProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'novelReaderTextColorStateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$novelReaderTextColorStateHash(); + + @$internal + @override + NovelReaderTextColorState create() => NovelReaderTextColorState(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(String value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$novelReaderTextColorStateHash() => + r'28a1987b49a9b0a209c4848dfa4c8c730432c75d'; + +abstract class _$NovelReaderTextColorState extends $Notifier { + String build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + String, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +@ProviderFor(NovelReaderPaddingState) +const novelReaderPaddingStateProvider = NovelReaderPaddingStateProvider._(); + +final class NovelReaderPaddingStateProvider + extends $NotifierProvider { + const NovelReaderPaddingStateProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'novelReaderPaddingStateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$novelReaderPaddingStateHash(); + + @$internal + @override + NovelReaderPaddingState create() => NovelReaderPaddingState(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(int value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$novelReaderPaddingStateHash() => + r'572f1a7134c499a9a5107d29552beca9a5fd55ea'; + +abstract class _$NovelReaderPaddingState extends $Notifier { + int build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + int, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +@ProviderFor(NovelReaderLineHeightState) +const novelReaderLineHeightStateProvider = + NovelReaderLineHeightStateProvider._(); + +final class NovelReaderLineHeightStateProvider + extends $NotifierProvider { + const NovelReaderLineHeightStateProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'novelReaderLineHeightStateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$novelReaderLineHeightStateHash(); + + @$internal + @override + NovelReaderLineHeightState create() => NovelReaderLineHeightState(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(double value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$novelReaderLineHeightStateHash() => + r'cc21fb550eecf8d7869c076ab47647afd2873996'; + +abstract class _$NovelReaderLineHeightState extends $Notifier { + double build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + double, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +@ProviderFor(NovelShowScrollPercentageState) +const novelShowScrollPercentageStateProvider = + NovelShowScrollPercentageStateProvider._(); + +final class NovelShowScrollPercentageStateProvider + extends $NotifierProvider { + const NovelShowScrollPercentageStateProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'novelShowScrollPercentageStateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$novelShowScrollPercentageStateHash(); + + @$internal + @override + NovelShowScrollPercentageState create() => NovelShowScrollPercentageState(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$novelShowScrollPercentageStateHash() => + r'adc9cb5def293fa4ed8b367929e7538f6f056b76'; + +abstract class _$NovelShowScrollPercentageState extends $Notifier { + bool build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + bool, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +@ProviderFor(NovelAutoScrollState) +const novelAutoScrollStateProvider = NovelAutoScrollStateProvider._(); + +final class NovelAutoScrollStateProvider + extends $NotifierProvider { + const NovelAutoScrollStateProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'novelAutoScrollStateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$novelAutoScrollStateHash(); + + @$internal + @override + NovelAutoScrollState create() => NovelAutoScrollState(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$novelAutoScrollStateHash() => + r'80f717515844fa97396dffc6f45ee0b7b9e6f96d'; + +abstract class _$NovelAutoScrollState extends $Notifier { + bool build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + bool, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +@ProviderFor(NovelRemoveExtraParagraphSpacingState) +const novelRemoveExtraParagraphSpacingStateProvider = + NovelRemoveExtraParagraphSpacingStateProvider._(); + +final class NovelRemoveExtraParagraphSpacingStateProvider + extends $NotifierProvider { + const NovelRemoveExtraParagraphSpacingStateProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'novelRemoveExtraParagraphSpacingStateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => + _$novelRemoveExtraParagraphSpacingStateHash(); + + @$internal + @override + NovelRemoveExtraParagraphSpacingState create() => + NovelRemoveExtraParagraphSpacingState(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$novelRemoveExtraParagraphSpacingStateHash() => + r'5c784a57ce5ee57524317dd00d4b40020e5e0582'; + +abstract class _$NovelRemoveExtraParagraphSpacingState extends $Notifier { + bool build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + bool, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} + +@ProviderFor(NovelTapToScrollState) +const novelTapToScrollStateProvider = NovelTapToScrollStateProvider._(); + +final class NovelTapToScrollStateProvider + extends $NotifierProvider { + const NovelTapToScrollStateProvider._() + : super( + from: null, + argument: null, + retry: null, + name: r'novelTapToScrollStateProvider', + isAutoDispose: true, + dependencies: null, + $allTransitiveDependencies: null, + ); + + @override + String debugGetCreateSourceHash() => _$novelTapToScrollStateHash(); + + @$internal + @override + NovelTapToScrollState create() => NovelTapToScrollState(); + + /// {@macro riverpod.override_with_value} + Override overrideWithValue(bool value) { + return $ProviderOverride( + origin: this, + providerOverride: $SyncValueProvider(value), + ); + } +} + +String _$novelTapToScrollStateHash() => + r'4ad09be8c324b019bd1d94cd8d77ef6077bd2100'; + +abstract class _$NovelTapToScrollState extends $Notifier { + bool build(); + @$mustCallSuper + @override + void runBuild() { + final created = build(); + final ref = this.ref as $Ref; + final element = + ref.element + as $ClassProviderElement< + AnyNotifier, + bool, + Object?, + Object? + >; + element.handleValue(ref, created); + } +} diff --git a/lib/modules/novel/novel_reader_controller_provider.dart b/lib/modules/novel/novel_reader_controller_provider.dart index 769682c8..4e4fc13e 100644 --- a/lib/modules/novel/novel_reader_controller_provider.dart +++ b/lib/modules/novel/novel_reader_controller_provider.dart @@ -1,10 +1,10 @@ import 'package:isar_community/isar.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; -import 'package:mangayomi/models/download.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; +import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'novel_reader_controller_provider.g.dart'; @@ -188,115 +188,3 @@ class NovelReaderController extends _$NovelReaderController { return chapter.name!; } } - -extension MangaExtensions on Manga { - List getFilteredChapterList() { - final data = this.chapters.toList().toList(); - final filterUnread = - (isar.settings - .getSync(227)! - .chapterFilterUnreadList! - .where((element) => element.mangaId == id) - .toList() - .firstOrNull ?? - ChapterFilterUnread(mangaId: id, type: 0)) - .type!; - - final filterBookmarked = - (isar.settings - .getSync(227)! - .chapterFilterBookmarkedList! - .where((element) => element.mangaId == id) - .toList() - .firstOrNull ?? - ChapterFilterBookmarked(mangaId: id, type: 0)) - .type!; - final filterDownloaded = - (isar.settings - .getSync(227)! - .chapterFilterDownloadedList! - .where((element) => element.mangaId == id) - .toList() - .firstOrNull ?? - ChapterFilterDownloaded(mangaId: id, type: 0)) - .type!; - - final sortChapter = - (isar.settings - .getSync(227)! - .sortChapterList! - .where((element) => element.mangaId == id) - .toList() - .firstOrNull ?? - SortChapter(mangaId: id, index: 1, reverse: false)) - .index; - final filterScanlator = _getFilterScanlator(this) ?? []; - List? chapterList; - chapterList = data - .where( - (element) => filterUnread == 1 - ? element.isRead == false - : filterUnread == 2 - ? element.isRead == true - : true, - ) - .where( - (element) => filterBookmarked == 1 - ? element.isBookmarked == true - : filterBookmarked == 2 - ? element.isBookmarked == false - : true, - ) - .where((element) { - final modelChapDownload = isar.downloads - .filter() - .idIsNotNull() - .idEqualTo(element.id) - .findAllSync(); - return filterDownloaded == 1 - ? modelChapDownload.isNotEmpty && - modelChapDownload.first.isDownload == true - : filterDownloaded == 2 - ? !(modelChapDownload.isNotEmpty && - modelChapDownload.first.isDownload == true) - : true; - }) - .where((element) => !filterScanlator.contains(element.scanlator)) - .toList(); - List chapters = sortChapter == 1 - ? chapterList.reversed.toList() - : chapterList; - if (sortChapter == 0) { - chapters.sort((a, b) { - return (a.scanlator == null || - b.scanlator == null || - a.dateUpload == null || - b.dateUpload == null) - ? 0 - : a.scanlator!.compareTo(b.scanlator!) | - a.dateUpload!.compareTo(b.dateUpload!); - }); - } else if (sortChapter == 2) { - chapters.sort((a, b) { - return (a.dateUpload == null || b.dateUpload == null) - ? 0 - : int.parse(a.dateUpload!).compareTo(int.parse(b.dateUpload!)); - }); - } else if (sortChapter == 3) { - chapters.sort((a, b) { - return (a.name == null || b.name == null) - ? 0 - : a.name!.compareTo(b.name!); - }); - } - return chapterList; - } -} - -List? _getFilterScanlator(Manga manga) { - final scanlators = isar.settings.getSync(227)!.filterScanlatorList ?? []; - final filter = scanlators - .where((element) => element.mangaId == manga.id) - .toList(); - return filter.firstOrNull?.scanlators; -} diff --git a/lib/modules/novel/novel_reader_view.dart b/lib/modules/novel/novel_reader_view.dart index dfbb1efe..ab419f8a 100644 --- a/lib/modules/novel/novel_reader_view.dart +++ b/lib/modules/novel/novel_reader_view.dart @@ -16,6 +16,8 @@ import 'package:mangayomi/modules/anime/widgets/desktop.dart'; import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/modules/novel/novel_reader_controller_provider.dart'; +import 'package:mangayomi/modules/novel/widgets/novel_reader_settings_sheet.dart'; +import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/services/get_html_content.dart'; import 'package:mangayomi/utils/extensions/dom_extensions.dart'; @@ -24,7 +26,7 @@ import 'package:mangayomi/modules/manga/reader/providers/push_router.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:window_manager/window_manager.dart'; -import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; +import 'package:flutter_html/flutter_html.dart'; import 'package:html/dom.dart' as dom; import 'package:flutter/widgets.dart' as widgets; @@ -73,6 +75,7 @@ class _NovelWebViewState extends ConsumerState if (_scrollController.hasClients) { offset = _scrollController.offset; maxOffset = _scrollController.position.maxScrollExtent; + _rebuildDetail.add(offset); } } @@ -203,119 +206,254 @@ class _NovelWebViewState extends ConsumerState bottom: false, child: Stack( children: [ - widget.result.when( - data: (data) { - epubBook = data.$2; - Future.delayed(const Duration(milliseconds: 1000), () { - if (!scrolled && _scrollController.hasClients) { - _scrollController.animateTo( - _scrollController.position.maxScrollExtent * - (double.tryParse(chapter.lastPageRead!) ?? 0), - duration: Duration(seconds: 2), - curve: Curves.fastOutSlowIn, - ); - scrolled = true; - } - }); - return Scrollbar( - controller: _scrollController, - interactive: true, - child: GestureDetector( - behavior: HitTestBehavior.translucent, - onTap: () { - _isViewFunction(); - }, - child: CustomScrollView( - controller: _scrollController, - physics: const BouncingScrollPhysics(), - slivers: [ - HtmlWidget( - data.$1, - customWidgetBuilder: (element) => - _buildCustomWidgets(element), - customStylesBuilder: (element) { - switch (backgroundColor) { - case BackgroundColor.black: - return {'background-color': 'black'}; - default: - return {'background-color': '#F0F0F0'}; - } - }, - onTapUrl: (url) { - context.push( - "/mangawebview", - extra: {'url': url, 'title': url}, - ); - return true; - }, - renderMode: RenderMode.sliverList, - textStyle: TextStyle( - color: backgroundColor == BackgroundColor.white - ? Colors.black - : Colors.white, - fontSize: fontSize.toDouble(), - ), - ), - SliverToBoxAdapter( - child: Center( - heightFactor: 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.center, - spacing: 5, - children: [ - IconButton( - padding: const EdgeInsets.all(5), - onPressed: () => - pushReplacementMangaReaderView( - context: context, - chapter: _readerController - .getPrevChapter(), - ), - icon: Icon( - size: 32, - Icons.arrow_back, - color: - backgroundColor == - BackgroundColor.white - ? Colors.black - : Colors.white, + Column( + children: [ + Flexible( + child: widget.result.when( + data: (data) { + epubBook = data.$2; + + final padding = ref.watch( + novelReaderPaddingStateProvider, + ); + final lineHeight = ref.watch( + novelReaderLineHeightStateProvider, + ); + final textAlign = ref.watch( + novelTextAlignStateProvider, + ); + final removeExtraSpacing = ref.watch( + novelRemoveExtraParagraphSpacingStateProvider, + ); + final customBackgroundColor = ref.watch( + novelReaderThemeStateProvider, + ); + final customTextColor = ref.watch( + novelReaderTextColorStateProvider, + ); + + Color parseColor(String hex) { + final hexColor = hex.replaceAll('#', ''); + return Color(int.parse('FF$hexColor', radix: 16)); + } + + TextAlign getTextAlign() { + switch (textAlign) { + case NovelTextAlign.left: + return TextAlign.left; + case NovelTextAlign.center: + return TextAlign.center; + case NovelTextAlign.right: + return TextAlign.right; + case NovelTextAlign.block: + return TextAlign.justify; + } + } + + Future.delayed(const Duration(milliseconds: 10), () { + if (!scrolled && _scrollController.hasClients) { + _scrollController.animateTo( + _scrollController.position.maxScrollExtent * + (double.tryParse(chapter.lastPageRead!) ?? + 0), + duration: Duration(seconds: 2), + curve: Curves.fastOutSlowIn, + ); + scrolled = true; + } + }); + return Consumer( + builder: (context, ref, _) { + final fontSize = ref.read( + novelFontSizeStateProvider, + ); + return Scrollbar( + controller: _scrollController, + interactive: true, + child: GestureDetector( + behavior: HitTestBehavior.translucent, + onTap: () { + _isViewFunction(); + }, + child: CustomScrollView( + controller: _scrollController, + physics: const BouncingScrollPhysics(), + slivers: [ + SliverToBoxAdapter( + child: Html( + data: data.$1, + style: { + "body": Style( + fontSize: FontSize( + fontSize.toDouble(), + ), + color: parseColor( + customTextColor, + ), + backgroundColor: parseColor( + customBackgroundColor, + ), + margin: Margins.zero, + padding: HtmlPaddings.all( + padding.toDouble(), + ), + lineHeight: LineHeight( + lineHeight, + ), + textAlign: getTextAlign(), + ), + "p": Style( + margin: removeExtraSpacing + ? Margins.only(bottom: 4) + : Margins.only(bottom: 8), + fontSize: FontSize( + fontSize.toDouble(), + ), + lineHeight: LineHeight( + lineHeight, + ), + textAlign: getTextAlign(), + ), + "div": Style( + fontSize: FontSize( + fontSize.toDouble(), + ), + lineHeight: LineHeight( + lineHeight, + ), + textAlign: getTextAlign(), + ), + "span": Style( + fontSize: FontSize( + fontSize.toDouble(), + ), + lineHeight: LineHeight( + lineHeight, + ), + ), + "h1, h2, h3, h4, h5, h6": Style( + color: parseColor( + customTextColor, + ), + lineHeight: LineHeight( + lineHeight, + ), + textAlign: getTextAlign(), + ), + "a": Style( + color: Colors.blue, + textDecoration: + TextDecoration.underline, + ), + "img": Style( + width: Width(100, Unit.percent), + height: Height.auto(), + ), + }, + extensions: [ + TagExtension( + tagsToExtend: {"img"}, + builder: (extensionContext) { + final element = + extensionContext.node + as dom.Element; + final customWidget = + _buildCustomWidgets( + element, + ); + if (customWidget != null) { + return customWidget; + } + + return const SizedBox.shrink(); + }, + ), + ], + onLinkTap: + (url, attributes, element) { + if (url != null) { + context.push( + "/mangawebview", + extra: { + 'url': url, + 'title': url, + }, + ); + } + }, + ), ), - ), - IconButton( - padding: const EdgeInsets.all(5), - onPressed: () => - pushReplacementMangaReaderView( - context: context, - chapter: _readerController - .getNextChapter(), - ), - icon: Icon( - size: 32, - Icons.arrow_forward, - color: - backgroundColor == - BackgroundColor.white - ? Colors.black - : Colors.white, - ), - ), - ], + ], + ), ), - ), - ), - ], + ); + }, + ); + }, + loading: () => scaffoldWith( + context, + Center(child: CircularProgressIndicator()), + ), + error: (err, stack) => scaffoldWith( + context, + Center(child: Text(err.toString())), ), ), - ); - }, - loading: () => scaffoldWith( - context, - Center(child: CircularProgressIndicator()), - ), - error: (err, stack) => scaffoldWith( - context, - Center(child: Text(err.toString())), - ), + ), + if (ref.watch(novelShowScrollPercentageStateProvider)) + StreamBuilder( + stream: _rebuildDetail.stream, + builder: (context, asyncSnapshot) { + return Consumer( + builder: (context, ref, child) { + final customBackgroundColor = ref.watch( + novelReaderThemeStateProvider, + ); + final customTextColor = ref.watch( + novelReaderTextColorStateProvider, + ); + final scrollPercentage = maxOffset > 0 + ? ((offset / maxOffset) * 100) + .clamp(0, 100) + .toInt() + : 0; + return Row( + children: [ + Expanded( + child: Container( + color: Color( + int.parse( + 'FF${customBackgroundColor.replaceAll('#', '')}', + radix: 16, + ), + ), + child: Center( + child: Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + '$scrollPercentage %', + style: TextStyle( + color: Color( + int.parse( + 'FF${customTextColor.replaceAll('#', '')}', + radix: 16, + ), + ), + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + ), + ), + ], + ); + }, + ); + }, + ), + ], ), _appBar(), _bottomBar(backgroundColor), @@ -485,70 +623,186 @@ class _NovelWebViewState extends ConsumerState _readerController.getChapterIndex().$2, ); bool hasNextChapter = _readerController.getChapterIndex().$1 != 0; - // final novelTextAlign = ref.watch(novelTextAlignStateProvider); // TODO. The variable is never used/modified - + final bodyLargeColor = Theme.of(context).textTheme.bodyLarge!.color; return Positioned( bottom: 0, child: AnimatedContainer( curve: Curves.ease, duration: const Duration(milliseconds: 300), width: context.width(1), - height: (_isView ? 130 : 0), + height: (_isView ? 140 : 0), child: Column( children: [ - Flexible( - child: Transform.scale( - scaleX: 1, - child: Row( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: CircleAvatar( - radius: 23, - backgroundColor: _backgroundColor(context), - child: IconButton( - onPressed: hasPrevChapter - ? () { - pushReplacementMangaReaderView( - context: context, - chapter: _readerController.getPrevChapter(), - ); - } - : null, - icon: Transform.scale( - scaleX: 1, - child: Icon( - Icons.skip_previous_rounded, - color: hasPrevChapter - ? Theme.of(context).textTheme.bodyLarge!.color - : Theme.of(context) - .textTheme - .bodyLarge! - .color! - .withValues(alpha: 0.4), - ), + if (_isView) + Row( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: CircleAvatar( + radius: 21, + backgroundColor: _backgroundColor(context), + child: IconButton( + onPressed: hasPrevChapter + ? () { + pushReplacementMangaReaderView( + context: context, + chapter: _readerController.getPrevChapter(), + ); + } + : null, + icon: Icon( + Icons.skip_previous_rounded, + color: hasPrevChapter + ? bodyLargeColor + : bodyLargeColor!.withValues(alpha: 0.4), + ), + ), + ), + ), + Flexible( + child: Container( + height: 40, + decoration: BoxDecoration( + color: _backgroundColor(context), + borderRadius: BorderRadius.circular(50), + ), + child: StreamBuilder( + stream: _rebuildDetail.stream, + builder: (context, asyncSnapshot) { + return Consumer( + builder: (context, ref, child) { + final scrollPercentage = maxOffset > 0 + ? ((offset / maxOffset) * 100) + .clamp(0, 100) + .toInt() + : 0; + return Row( + children: [ + SizedBox(width: 10), + Padding( + padding: const EdgeInsets.all(4), + child: Text( + scrollPercentage.toInt().toString(), + style: TextStyle( + color: bodyLargeColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + if (_isView) + Expanded( + flex: 14, + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 2.0, + thumbShape: + const RoundSliderThumbShape( + enabledThumbRadius: 6.0, + ), + overlayShape: + const RoundSliderOverlayShape( + overlayRadius: 12.0, + ), + ), + child: Slider( + onChanged: (value) { + _scrollController.jumpTo( + _scrollController + .position + .maxScrollExtent * + value, + ); + }, + value: scrollPercentage / 100, + min: 0, + max: 1, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(4.0), + child: Text( + '100', + style: TextStyle( + color: bodyLargeColor, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + ), + SizedBox(width: 10), + ], + ); + }, + ); + }, + ), + ), + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: CircleAvatar( + radius: 21, + backgroundColor: _backgroundColor(context), + child: IconButton( + onPressed: hasNextChapter + ? () { + pushReplacementMangaReaderView( + context: context, + chapter: _readerController.getNextChapter(), + ); + } + : null, + icon: Transform.scale( + scaleX: 1, + child: Icon( + Icons.skip_next_rounded, + color: hasNextChapter + ? bodyLargeColor + : bodyLargeColor!.withValues(alpha: 0.4), ), ), ), ), - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Container( - height: 70, - decoration: BoxDecoration( - color: _backgroundColor(context), - borderRadius: BorderRadius.circular(25), - ), + ), + ], + ), + if (_isView) + Expanded( + child: Container( + color: _backgroundColor(context), + child: Row( + children: [ + Flexible( + child: SizedBox( + height: 50, child: Row( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Transform.scale( - scaleX: 1, - child: SizedBox( - width: 55, - child: Center( - child: IconButton( + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + border: Border.all( + color: bodyLargeColor!, + width: 0.2, + ), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Text Size :', + style: TextStyle( + fontWeight: FontWeight.bold, + color: bodyLargeColor, + ), + ), + IconButton( onPressed: () { final newFontSize = max( 4, @@ -564,58 +818,52 @@ class _NovelWebViewState extends ConsumerState fontSize = newFontSize; }); }, - icon: const Icon(Icons.text_decrease), + icon: Icon(Icons.text_decrease), + iconSize: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 40, + minHeight: 40, + ), ), - ), - ), - ), - if (_isView) - Flexible( - flex: 14, - child: Consumer( - builder: (context, ref, child) { - final currentFontSize = ref.watch( - novelFontSizeStateProvider, - ); - return SliderTheme( - data: SliderTheme.of(context).copyWith( - overlayShape: - const RoundSliderOverlayShape( - overlayRadius: 5.0, + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 5, + ), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Theme.of(context) + .colorScheme + .primaryContainer + .withValues(alpha: 0.5), + borderRadius: BorderRadius.circular( + 8, + ), + ), + child: Consumer( + builder: (context, ref, child) { + final currentFontSize = ref.watch( + novelFontSizeStateProvider, + ); + return Text( + "$currentFontSize px", + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, ), - ), - child: Slider( - onChanged: (value) { - ref - .read( - novelFontSizeStateProvider - .notifier, - ) - .set(value.toInt()); + ); }, - onChangeEnd: (newValue) { - try { - setState(() { - fontSize = newValue.toInt(); - }); - } catch (_) {} - }, - divisions: 36, - value: currentFontSize.toDouble(), - label: "$currentFontSize", - min: 4, - max: 40, ), - ); - }, - ), - ), - Transform.scale( - scaleX: 1, - child: SizedBox( - width: 55, - child: Center( - child: IconButton( + ), + ), + IconButton( onPressed: () { final newFontSize = min( 40, @@ -632,106 +880,42 @@ class _NovelWebViewState extends ConsumerState }); }, icon: const Icon(Icons.text_increase), + iconSize: 20, + padding: EdgeInsets.zero, + constraints: const BoxConstraints( + minWidth: 40, + minHeight: 40, + ), ), - ), + ], ), ), + + IconButton( + onPressed: () { + customDraggableTabBar( + tabs: [ + Tab(text: context.l10n.reader), + Tab(text: context.l10n.general), + ], + children: [ + ReaderSettingsTab(), + GeneralSettingsTab(), + ], + context: context, + vsync: this, + ); + }, + icon: const Icon(Icons.settings), + ), ], ), ), ), - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: CircleAvatar( - radius: 23, - backgroundColor: _backgroundColor(context), - child: IconButton( - onPressed: hasNextChapter - ? () { - pushReplacementMangaReaderView( - context: context, - chapter: _readerController.getNextChapter(), - ); - } - : null, - icon: Transform.scale( - scaleX: 1, - child: Icon( - Icons.skip_next_rounded, - color: hasNextChapter - ? Theme.of(context).textTheme.bodyLarge!.color - : Theme.of(context) - .textTheme - .bodyLarge! - .color! - .withValues(alpha: 0.4), - // size: 17, - ), - ), - ), - ), - ), - ], + ], + ), ), ), - ), - /*Flexible( - child: Container( - height: 65, - color: _backgroundColor(context), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - PopupMenuButton( - popUpAnimationStyle: popupAnimationStyle, - color: Colors.black, - child: const Icon( - Icons.format_align_center_outlined, - ), - onSelected: (value) { - ref - .read(novelTextAlignStateProvider.notifier) - .set(value); - }, - itemBuilder: (context) => [ - for (var mode in NovelTextAlign.values) - PopupMenuItem( - value: mode, - child: Row( - children: [ - Icon( - Icons.check, - color: novelTextAlign == mode - ? Colors.white - : Colors.transparent, - ), - const SizedBox( - width: 7, - ), - Text( - mode.name, - style: const TextStyle( - color: Colors.white, - fontSize: 12, - ), - ), - ], - )), - ], - ), - IconButton( - onPressed: () { - // _showModalSettings(); - }, - icon: const Icon( - Icons.settings_rounded, - ), - ), - ], - ), - ), - ),*/ ], ), ), diff --git a/lib/modules/novel/widgets/novel_reader_settings_sheet.dart b/lib/modules/novel/widgets/novel_reader_settings_sheet.dart new file mode 100644 index 00000000..9bccae4c --- /dev/null +++ b/lib/modules/novel/widgets/novel_reader_settings_sheet.dart @@ -0,0 +1,591 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mangayomi/models/settings.dart'; +import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; + +class ReaderSettingsTab extends ConsumerWidget { + const ReaderSettingsTab({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final padding = ref.watch(novelReaderPaddingStateProvider); + final lineHeight = ref.watch(novelReaderLineHeightStateProvider); + final textAlign = ref.watch(novelTextAlignStateProvider); + final backgroundColor = ref.watch(novelReaderThemeStateProvider); + final textColor = ref.watch(novelReaderTextColorStateProvider); + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _SettingSection( + title: 'Theme', + child: Column( + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _ThemeButton( + backgroundColor: '#292832', + textColor: '#CCCCCC', + label: 'Dark', + isSelected: backgroundColor == '#292832', + onTap: () { + ref + .read(novelReaderThemeStateProvider.notifier) + .set('#292832'); + ref + .read(novelReaderTextColorStateProvider.notifier) + .set('#CCCCCC'); + }, + ), + _ThemeButton( + backgroundColor: '#FFFFFF', + textColor: '#000000', + label: 'Light', + isSelected: backgroundColor == '#FFFFFF', + onTap: () { + ref + .read(novelReaderThemeStateProvider.notifier) + .set('#FFFFFF'); + ref + .read(novelReaderTextColorStateProvider.notifier) + .set('#000000'); + }, + ), + _ThemeButton( + backgroundColor: '#000000', + textColor: '#FFFFFF', + label: 'Black', + isSelected: backgroundColor == '#000000', + onTap: () { + ref + .read(novelReaderThemeStateProvider.notifier) + .set('#000000'); + ref + .read(novelReaderTextColorStateProvider.notifier) + .set('#FFFFFF'); + }, + ), + _ThemeButton( + backgroundColor: '#F5E6D3', + textColor: '#5F4B32', + label: 'Sepia', + isSelected: backgroundColor == '#F5E6D3', + onTap: () { + ref + .read(novelReaderThemeStateProvider.notifier) + .set('#F5E6D3'); + ref + .read(novelReaderTextColorStateProvider.notifier) + .set('#5F4B32'); + }, + ), + ], + ), + const SizedBox(height: 12), + Row( + children: [ + Expanded( + child: _ColorPicker( + label: 'Background', + color: backgroundColor, + onColorChanged: (color) { + ref + .read(novelReaderThemeStateProvider.notifier) + .set(color); + }, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _ColorPicker( + label: 'Text', + color: textColor, + onColorChanged: (color) { + ref + .read(novelReaderTextColorStateProvider.notifier) + .set(color); + }, + ), + ), + ], + ), + ], + ), + ), + + const SizedBox(height: 16), + _SettingSection( + title: 'Text Align', + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _AlignButton( + icon: Icons.format_align_left, + isSelected: textAlign == NovelTextAlign.left, + onTap: () { + ref + .read(novelTextAlignStateProvider.notifier) + .set(NovelTextAlign.left); + }, + ), + _AlignButton( + icon: Icons.format_align_center, + isSelected: textAlign == NovelTextAlign.center, + onTap: () { + ref + .read(novelTextAlignStateProvider.notifier) + .set(NovelTextAlign.center); + }, + ), + _AlignButton( + icon: Icons.format_align_right, + isSelected: textAlign == NovelTextAlign.right, + onTap: () { + ref + .read(novelTextAlignStateProvider.notifier) + .set(NovelTextAlign.right); + }, + ), + _AlignButton( + icon: Icons.format_align_justify, + isSelected: textAlign == NovelTextAlign.block, + onTap: () { + ref + .read(novelTextAlignStateProvider.notifier) + .set(NovelTextAlign.block); + }, + ), + ], + ), + ), + + const SizedBox(height: 16), + + _SettingSection( + title: 'Padding', + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.space_bar, size: 20), + Expanded( + child: Slider( + value: padding.toDouble(), + min: 0, + max: 50, + divisions: 50, + label: '$padding px', + onChanged: (value) { + ref + .read(novelReaderPaddingStateProvider.notifier) + .set(value.toInt()); + }, + ), + ), + Text('${padding}px'), + ], + ), + ], + ), + ), + + const SizedBox(height: 16), + + _SettingSection( + title: 'Line Height', + child: Column( + children: [ + Row( + children: [ + const Icon(Icons.height, size: 20), + Expanded( + child: Slider( + value: lineHeight, + min: 1.0, + max: 3.0, + divisions: 20, + label: lineHeight.toStringAsFixed(1), + onChanged: (value) { + ref + .read(novelReaderLineHeightStateProvider.notifier) + .set(value); + }, + ), + ), + Text(lineHeight.toStringAsFixed(1)), + ], + ), + ], + ), + ), + ], + ), + ); + } +} + +class GeneralSettingsTab extends ConsumerWidget { + const GeneralSettingsTab({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.all(16), + child: Column( + children: [ + _SwitchListTileSetting( + title: 'Show Scroll Percentage', + subtitle: 'Display reading progress percentage', + value: ref.watch(novelShowScrollPercentageStateProvider), + onChanged: (value) { + ref + .read(novelShowScrollPercentageStateProvider.notifier) + .set(value); + }, + ), + + // _SwitchListTileSetting( + // title: 'Auto Scroll', + // subtitle: 'Automatically scroll through pages', + // value: ref.watch(novelAutoScrollStateProvider), + // onChanged: (value) { + // ref.read(novelAutoScrollStateProvider.notifier).set(value); + // }, + // ), + _SwitchListTileSetting( + title: 'Remove Extra Paragraph Spacing', + subtitle: 'Reduce spacing between paragraphs', + value: ref.watch(novelRemoveExtraParagraphSpacingStateProvider), + onChanged: (value) { + ref + .read(novelRemoveExtraParagraphSpacingStateProvider.notifier) + .set(value); + }, + ), + + // _SwitchListTileSetting( + // title: 'Tap to Scroll', + // subtitle: 'Tap screen to scroll up/down', + // value: ref.watch(novelTapToScrollStateProvider), + // onChanged: (value) { + // ref.read(novelTapToScrollStateProvider.notifier).set(value); + // }, + // ), + ], + ), + ); + } +} + +class _SettingSection extends StatelessWidget { + final String title; + final Widget child; + + const _SettingSection({required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.titleLarge?.color, + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), + child: child, + ), + ), + ], + ); + } +} + +class _SwitchListTileSetting extends StatelessWidget { + final String title; + final String subtitle; + final bool value; + final ValueChanged onChanged; + + const _SwitchListTileSetting({ + required this.title, + required this.subtitle, + required this.value, + required this.onChanged, + }); + + @override + Widget build(BuildContext context) { + return SwitchListTile( + title: Text(title), + subtitle: Text(subtitle, style: Theme.of(context).textTheme.bodySmall), + value: value, + onChanged: onChanged, + ); + } +} + +class _ThemeButton extends StatelessWidget { + final String backgroundColor; + final String textColor; + final String label; + final bool isSelected; + final VoidCallback onTap; + + const _ThemeButton({ + required this.backgroundColor, + required this.textColor, + required this.label, + required this.isSelected, + required this.onTap, + }); + + Color _parseColor(String hex) { + final hexColor = hex.replaceAll('#', ''); + return Color(int.parse('FF$hexColor', radix: 16)); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 70, + height: 60, + decoration: BoxDecoration( + color: _parseColor(backgroundColor), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected + ? Theme.of(context).primaryColor + : Colors.grey.withValues(alpha: 0.3), + width: isSelected ? 3 : 1, + ), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Aa', + style: TextStyle( + color: _parseColor(textColor), + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + label, + style: TextStyle(color: _parseColor(textColor), fontSize: 10), + ), + ], + ), + ), + ); + } +} + +class _ColorPicker extends StatelessWidget { + final String label; + final String color; + final ValueChanged onColorChanged; + + const _ColorPicker({ + required this.label, + required this.color, + required this.onColorChanged, + }); + + Color _parseColor(String hex) { + final hexColor = hex.replaceAll('#', ''); + return Color(int.parse('FF$hexColor', radix: 16)); + } + + String _colorToHex(Color color) { + return '#${color.toARGB32().toRadixString(16).substring(2).toUpperCase()}'; + } + + void _showColorPickerDialog(BuildContext context) { + Color selectedColor = _parseColor(color); + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text('Select $label Color'), + content: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + _colorOption(context, Colors.white, selectedColor), + _colorOption(context, Colors.black, selectedColor), + _colorOption( + context, + const Color(0xFF292832), + selectedColor, + ), + _colorOption( + context, + const Color(0xFFF5E6D3), + selectedColor, + ), + _colorOption( + context, + const Color(0xFF5F4B32), + selectedColor, + ), + _colorOption( + context, + const Color(0xFFCCCCCC), + selectedColor, + ), + _colorOption(context, Colors.grey[800]!, selectedColor), + _colorOption(context, Colors.grey[300]!, selectedColor), + _colorOption(context, Colors.brown[100]!, selectedColor), + _colorOption(context, Colors.blue[100]!, selectedColor), + _colorOption(context, Colors.green[100]!, selectedColor), + _colorOption(context, Colors.amber[100]!, selectedColor), + ], + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ], + ); + }, + ); + } + + Widget _colorOption( + BuildContext context, + Color optionColor, + Color selectedColor, + ) { + final isSelected = optionColor.toARGB32() == selectedColor.toARGB32(); + return GestureDetector( + onTap: () { + onColorChanged(_colorToHex(optionColor)); + Navigator.of(context).pop(); + }, + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: optionColor, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected ? Theme.of(context).primaryColor : Colors.grey, + width: isSelected ? 3 : 1, + ), + ), + child: isSelected + ? Icon( + Icons.check, + color: optionColor.computeLuminance() > 0.5 + ? Colors.black + : Colors.white, + ) + : null, + ), + ); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => _showColorPickerDialog(context), + child: Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.withValues(alpha: 0.3)), + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: _parseColor(color), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: Colors.grey), + ), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: const TextStyle(fontSize: 12)), + Text( + color, + style: TextStyle(fontSize: 10, color: Colors.grey[600]), + ), + ], + ), + ), + const Icon(Icons.arrow_drop_down), + ], + ), + ), + ); + } +} + +class _AlignButton extends StatelessWidget { + final IconData icon; + final bool isSelected; + final VoidCallback onTap; + + const _AlignButton({ + required this.icon, + required this.isSelected, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).primaryColor.withValues(alpha: 0.2) + : Colors.transparent, + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: isSelected + ? Theme.of(context).primaryColor + : Colors.grey.withValues(alpha: 0.3), + width: isSelected ? 2 : 1, + ), + ), + child: Icon( + icon, + color: isSelected + ? Theme.of(context).primaryColor + : Theme.of(context).iconTheme.color, + ), + ), + ); + } +} diff --git a/lib/modules/widgets/custom_extended_image_provider.dart b/lib/modules/widgets/custom_extended_image_provider.dart index 90248ab0..43c96938 100644 --- a/lib/modules/widgets/custom_extended_image_provider.dart +++ b/lib/modules/widgets/custom_extended_image_provider.dart @@ -14,6 +14,130 @@ import 'package:path_provider/path_provider.dart'; import 'package:extended_image_library/src/network/extended_network_image_provider.dart' as image_provider; +/// LRU Memory Cache for decoded image data +class _LRUCache { + final int _maxSize; + final _cache = {}; + int _currentSize = 0; + final int Function(V)? _sizeOf; + + _LRUCache({required int maxSize, int Function(V)? sizeOf}) + : _maxSize = maxSize, + _sizeOf = sizeOf; + + V? get(K key) { + final value = _cache.remove(key); + if (value != null) { + _cache[key] = value; // Move to end (most recently used) + } + return value; + } + + void put(K key, V value) { + _cache.remove(key); // Remove if exists + _cache[key] = value; // Add to end + + if (_sizeOf != null) { + _currentSize += _sizeOf(value); + while (_currentSize > _maxSize && _cache.isNotEmpty) { + final oldest = _cache.entries.first; + _currentSize -= _sizeOf(oldest.value); + _cache.remove(oldest.key); + } + } else { + while (_cache.length > _maxSize) { + _cache.remove(_cache.keys.first); + } + } + } + + void remove(K key) { + final value = _cache.remove(key); + if (value != null && _sizeOf != null) { + _currentSize -= _sizeOf(value); + } + } + + void clear() { + _cache.clear(); + _currentSize = 0; + } + + int get length => _cache.length; + int get currentSize => _currentSize; +} + +/// Global memory cache (100 images max, ~50MB) +final _memoryCache = _LRUCache( + maxSize: 50 * 1024 * 1024, // 50MB + sizeOf: (data) => data.length, +); + +/// Cache metadata for LRU eviction +class _CacheMetadata { + final String path; + final int size; + final DateTime lastAccessed; + + _CacheMetadata({ + required this.path, + required this.size, + required this.lastAccessed, + }); +} + +/// Global cache manager +class _CacheManager { + static const _maxCacheSize = 500 * 1024 * 1024; // 500MB + + static Future getCacheSize(Directory cacheDir) async { + if (!await cacheDir.exists()) return 0; + + int totalSize = 0; + await for (final entity in cacheDir.list(recursive: true)) { + if (entity is File) { + totalSize += await entity.length(); + } + } + return totalSize; + } + + static Future evictOldestIfNeeded(Directory cacheDir) async { + final size = await getCacheSize(cacheDir); + if (size <= _maxCacheSize) return; + + // Collect all cache files with metadata + final List<_CacheMetadata> files = []; + await for (final entity in cacheDir.list()) { + if (entity is File) { + final stat = await entity.stat(); + files.add( + _CacheMetadata( + path: entity.path, + size: stat.size, + lastAccessed: stat.accessed, + ), + ); + } + } + + // Sort by last accessed (oldest first) + files.sort((a, b) => a.lastAccessed.compareTo(b.lastAccessed)); + + // Delete until under limit + int currentSize = size; + for (final file in files) { + if (currentSize <= _maxCacheSize) break; + try { + await File(file.path).delete(); + currentSize -= file.size; + } catch (e) { + if (kDebugMode) print('Failed to delete cache file: $e'); + } + } + } +} + class CustomExtendedNetworkImageProvider extends ImageProvider with ExtendedImageProvider @@ -187,6 +311,12 @@ class CustomExtendedNetworkImageProvider StreamController? chunkEvents, String md5Key, ) async { + // Check memory cache first + final cachedData = _memoryCache.get(md5Key); + if (cachedData != null) { + return cachedData; + } + final Directory cacheImagesDirectory = Directory( join( (await getTemporaryDirectory()).path, @@ -197,6 +327,7 @@ class CustomExtendedNetworkImageProvider Uint8List? data; await StorageProvider().createDirectorySafely(cacheImagesDirectory.path); final File cacheFile = File(join(cacheImagesDirectory.path, md5Key)); + // exist, try to find cache image file if (cacheFile.existsSync()) { if (key.cacheMaxAge != null) { @@ -206,17 +337,28 @@ class CustomExtendedNetworkImageProvider cacheFile.deleteSync(); } else { data = await cacheFile.readAsBytes(); + // Store in memory cache + _memoryCache.put(md5Key, data); } } else { data = await cacheFile.readAsBytes(); + // Store in memory cache + _memoryCache.put(md5Key, data); } } + // load from network if (data == null) { data = await _loadNetwork(key, chunkEvents); if (data != null) { + // Evict old cache if needed before writing + await _CacheManager.evictOldestIfNeeded(cacheImagesDirectory); + // cache image file await File(join(cacheImagesDirectory.path, md5Key)).writeAsBytes(data); + + // Store in memory cache + _memoryCache.put(md5Key, data); } } @@ -231,32 +373,37 @@ class CustomExtendedNetworkImageProvider try { final Uri resolved = Uri.base.resolve(key.url); final StreamedResponse? response = await _tryGetResponse(resolved); - List bytes = []; - final int total = response!.contentLength ?? 0; - if (response.statusCode == HttpStatus.ok) { - int received = 0; - response.stream.asBroadcastStream(); - await for (var chunk in response.stream) { - bytes.addAll(chunk); - try { - received += chunk.length; - if (chunkEvents != null) {} - chunkEvents!.add( - ImageChunkEvent( - cumulativeBytesLoaded: received, - expectedTotalBytes: total, - ), - ); - } catch (e) { - if (kDebugMode) { - print(e); - } - } - } - } else { + + if (response == null || response.statusCode != HttpStatus.ok) { return null; } + // Pre-allocate list if content length is known + final int total = response.contentLength ?? 0; + final List bytes = total > 0 + ? List.filled(total, 0, growable: true) + : []; + int received = 0; + + response.stream.asBroadcastStream(); + await for (var chunk in response.stream) { + if (total > 0 && received + chunk.length <= total) { + // Copy directly to pre-allocated list + bytes.setRange(received, received + chunk.length, chunk); + } else { + // Fallback for unknown size + bytes.addAll(chunk); + } + + received += chunk.length; + chunkEvents?.add( + ImageChunkEvent( + cumulativeBytesLoaded: received, + expectedTotalBytes: total, + ), + ); + } + if (bytes.isEmpty) { return Future.error( StateError('NetworkImage is an empty file: $resolved'), @@ -281,11 +428,20 @@ class CustomExtendedNetworkImageProvider Future _getResponse(Uri resolved) async { var request = Request('GET', resolved); - request.headers.addAll(headers ?? {}); + + // Optimize headers for better caching and compression + final optimizedHeaders = { + ...?headers, + 'Accept-Encoding': 'gzip, deflate, br', + 'Accept': 'image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', + 'Connection': 'keep-alive', + }; + request.headers.addAll(optimizedHeaders); StreamedResponse response = await MClient.init( showCloudFlareError: showCloudFlareError, ).send(request); + if (response.statusCode != 200) { final res = await MClient.init( reqcopyWith: {'useDartHttpClient': true}, @@ -297,20 +453,40 @@ class CustomExtendedNetworkImageProvider return response; } - // Http get with cancel, delay try again + // Http get with cancel, exponential backoff retry Future _tryGetResponse(Uri resolved) async { cancelToken?.throwIfCancellationRequested(); - return await RetryHelper.tryRun( - () { - return CancellationTokenSource.register( + + int attempt = 0; + while (attempt < retries) { + try { + return await CancellationTokenSource.register( cancelToken, _getResponse(resolved), ); - }, - cancelToken: cancelToken, - timeRetry: timeRetry, - retries: retries, - ); + } catch (e) { + attempt++; + if (attempt >= retries) { + rethrow; + } + + // Exponential backoff: 100ms, 200ms, 400ms, 800ms, etc. + final backoffDelay = Duration( + milliseconds: timeRetry.inMilliseconds * (1 << attempt), + ); + + if (kDebugMode) { + print( + 'Retry attempt $attempt/$retries after ${backoffDelay.inMilliseconds}ms', + ); + } + + await Future.delayed(backoffDelay); + cancelToken?.throwIfCancellationRequested(); + } + } + + return null; } @override diff --git a/lib/services/fetch_item_sources.dart b/lib/services/fetch_item_sources.dart index 7a887f50..36a6a03a 100644 --- a/lib/services/fetch_item_sources.dart +++ b/lib/services/fetch_item_sources.dart @@ -15,13 +15,16 @@ Future fetchItemSourcesList( if (ref.watch(checkForExtensionsUpdateStateProvider) || reFresh) { final repos = ref.watch(extensionsRepoStateProvider(itemType)); for (Repo repo in repos) { - await fetchSourcesList( - repo: repo, - refresh: reFresh, - id: id, - ref: ref, - itemType: itemType, - ); + try { + await fetchSourcesList( + repo: repo, + refresh: reFresh, + id: id, + androidProxyServer: ref.watch(androidProxyServerStateProvider), + autoUpdateExtensions: ref.watch(autoUpdateExtensionsStateProvider), + itemType: itemType, + ); + } catch (_) {} } } } diff --git a/lib/services/fetch_item_sources.g.dart b/lib/services/fetch_item_sources.g.dart index ec27c992..00cdc767 100644 --- a/lib/services/fetch_item_sources.g.dart +++ b/lib/services/fetch_item_sources.g.dart @@ -65,7 +65,7 @@ final class FetchItemSourcesListProvider } String _$fetchItemSourcesListHash() => - r'16238be20517fddacf52a2694fbd50cafbfa7496'; + r'219aed67d2329f03101f2270e2f344bf70eff128'; final class FetchItemSourcesListFamily extends $Family with diff --git a/lib/services/fetch_sources_list.dart b/lib/services/fetch_sources_list.dart index 00fb03ac..c23b0ed5 100644 --- a/lib/services/fetch_sources_list.dart +++ b/lib/services/fetch_sources_list.dart @@ -1,5 +1,4 @@ import 'dart:convert'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http_interceptor/http_interceptor.dart'; import 'package:isar_community/isar.dart'; import 'package:mangayomi/eval/lib.dart'; @@ -9,14 +8,14 @@ import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/settings.dart'; import 'package:mangayomi/models/source.dart'; -import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; import 'package:mangayomi/services/http/m_client.dart'; import 'package:package_info_plus/package_info_plus.dart'; Future fetchSourcesList({ int? id, required bool refresh, - required Ref ref, + required String androidProxyServer, + required bool autoUpdateExtensions, required ItemType itemType, required Repo? repo, }) async { @@ -126,21 +125,21 @@ Future fetchSourcesList({ orElse: () => Source(), ); if (matchingSource.id != null && matchingSource.sourceCodeUrl!.isNotEmpty) { - await _updateSource(matchingSource, ref, repo, itemType); + await _updateSource(matchingSource, androidProxyServer, repo, itemType); } } else { for (var source in sourceList) { final existingSource = await isar.sources.get(source.id!); if (existingSource == null) { - await _addNewSource(source, ref, repo, itemType); + await _addNewSource(source, repo, itemType); continue; } final shouldUpdate = existingSource.isAdded! && compareVersions(existingSource.version!, source.version!) < 0; if (!shouldUpdate) continue; - if (ref.read(autoUpdateExtensionsStateProvider)) { - await _updateSource(source, ref, repo, itemType); + if (autoUpdateExtensions) { + await _updateSource(source, androidProxyServer, repo, itemType); } else { await isar.writeTxn(() async { isar.sources.put(existingSource..versionLast = source.version); @@ -149,12 +148,12 @@ Future fetchSourcesList({ } } - checkIfSourceIsObsolete(sourceList, repo!, itemType, ref); + checkIfSourceIsObsolete(sourceList, repo!, itemType); } Future _updateSource( Source source, - Ref ref, + String androidProxyServer, Repo? repo, ItemType itemType, ) async { @@ -163,7 +162,7 @@ Future _updateSource( final sourceCode = source.sourceCodeLanguage == SourceCodeLanguage.mihon ? base64.encode(req.bodyBytes) : req.body; - final androidProxyServer = ref.read(androidProxyServerStateProvider); + Map headers = {}; bool? supportLatest; FilterList? filterList; @@ -232,12 +231,7 @@ Future _updateSource( await isar.writeTxn(() async => isar.sources.put(updatedSource)); } -Future _addNewSource( - Source source, - Ref ref, - Repo? repo, - ItemType itemType, -) async { +Future _addNewSource(Source source, Repo? repo, ItemType itemType) async { final newSource = Source() ..sourceCodeUrl = source.sourceCodeUrl ..id = source.id @@ -269,7 +263,6 @@ Future checkIfSourceIsObsolete( List sourceList, Repo repo, ItemType itemType, - Ref ref, ) async { if (sourceList.isEmpty) return; diff --git a/lib/services/get_chapter_pages.g.dart b/lib/services/get_chapter_pages.g.dart index 216be780..609f2ca6 100644 --- a/lib/services/get_chapter_pages.g.dart +++ b/lib/services/get_chapter_pages.g.dart @@ -66,7 +66,7 @@ final class GetChapterPagesProvider } } -String _$getChapterPagesHash() => r'129624607a92b6d3a896a03b450862ce1e941ff6'; +String _$getChapterPagesHash() => r'dab1776f81d5ef5003d4d4515fe634f56c14b795'; final class GetChapterPagesFamily extends $Family with $FunctionalFamilyOverride, Chapter> { diff --git a/lib/services/get_detail.dart b/lib/services/get_detail.dart index 210c85a8..25f3bf27 100644 --- a/lib/services/get_detail.dart +++ b/lib/services/get_detail.dart @@ -1,7 +1,8 @@ -import 'package:mangayomi/eval/lib.dart'; +import 'dart:async'; import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; +import 'package:mangayomi/services/isolate_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'get_detail.g.dart'; @@ -11,8 +12,12 @@ Future getDetail( required String url, required Source source, }) async { - return getExtensionService( - source, - ref.read(androidProxyServerStateProvider), - ).getDetail(url); + final proxyServer = ref.read(androidProxyServerStateProvider); + + return getIsolateService.get( + url: url, + source: source, + serviceType: 'getDetail', + proxyServer: proxyServer, + ); } diff --git a/lib/services/get_detail.g.dart b/lib/services/get_detail.g.dart index 25421680..0e4269ab 100644 --- a/lib/services/get_detail.g.dart +++ b/lib/services/get_detail.g.dart @@ -58,7 +58,7 @@ final class GetDetailProvider } } -String _$getDetailHash() => r'6b758b79281cb00a7df2fe1903d4a67068052bca'; +String _$getDetailHash() => r'7eab7d00e6ad61a9bafaee855eae1f49b127af9f'; final class GetDetailFamily extends $Family with diff --git a/lib/services/get_html_content.g.dart b/lib/services/get_html_content.g.dart index 9bbcdfc2..4eeb6664 100644 --- a/lib/services/get_html_content.g.dart +++ b/lib/services/get_html_content.g.dart @@ -66,7 +66,7 @@ final class GetHtmlContentProvider } } -String _$getHtmlContentHash() => r'fa74506c0adebbdb7a0dda5a8d16a784466b79bb'; +String _$getHtmlContentHash() => r'a5763e11960bfe0dbd38ce2b2a3f4b51fefc976e'; final class GetHtmlContentFamily extends $Family with $FunctionalFamilyOverride, Chapter> { diff --git a/lib/services/get_latest_updates.dart b/lib/services/get_latest_updates.dart index 424918be..cc43ca4f 100644 --- a/lib/services/get_latest_updates.dart +++ b/lib/services/get_latest_updates.dart @@ -1,13 +1,12 @@ import 'dart:math'; - import 'package:isar_community/isar.dart'; -import 'package:mangayomi/eval/lib.dart'; import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/eval/model/m_pages.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; +import 'package:mangayomi/services/isolate_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'get_latest_updates.g.dart'; @@ -38,8 +37,10 @@ Future getLatestUpdates( .toList(); return MPages(list: result, hasNextPage: true); } - return getExtensionService( - source, - ref.read(androidProxyServerStateProvider), - ).getLatestUpdates(page); + return getIsolateService.get( + page: page, + source: source, + serviceType: 'getLatestUpdates', + proxyServer: ref.read(androidProxyServerStateProvider), + ); } diff --git a/lib/services/get_latest_updates.g.dart b/lib/services/get_latest_updates.g.dart index a288a5d8..8e3b0040 100644 --- a/lib/services/get_latest_updates.g.dart +++ b/lib/services/get_latest_updates.g.dart @@ -58,7 +58,7 @@ final class GetLatestUpdatesProvider } } -String _$getLatestUpdatesHash() => r'7a3c06c469c77ec933cf2f4dd7d39780d993f0ea'; +String _$getLatestUpdatesHash() => r'6f99dfe1d4aa950b6852110ec23f92b5c73c413c'; final class GetLatestUpdatesFamily extends $Family with diff --git a/lib/services/get_popular.dart b/lib/services/get_popular.dart index 3483db6a..204bd2eb 100644 --- a/lib/services/get_popular.dart +++ b/lib/services/get_popular.dart @@ -1,13 +1,12 @@ import 'dart:math'; - import 'package:isar_community/isar.dart'; -import 'package:mangayomi/eval/lib.dart'; import 'package:mangayomi/eval/model/m_manga.dart'; import 'package:mangayomi/eval/model/m_pages.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/modules/more/settings/browse/providers/browse_state_provider.dart'; +import 'package:mangayomi/services/isolate_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'get_popular.g.dart'; @@ -38,8 +37,11 @@ Future getPopular( .toList(); return MPages(list: result, hasNextPage: true); } - return getExtensionService( - source, - ref.read(androidProxyServerStateProvider), - ).getPopular(page); + + return getIsolateService.get( + page: page, + source: source, + serviceType: 'getPopular', + proxyServer: ref.read(androidProxyServerStateProvider), + ); } diff --git a/lib/services/get_popular.g.dart b/lib/services/get_popular.g.dart index d1130b91..e2b97d30 100644 --- a/lib/services/get_popular.g.dart +++ b/lib/services/get_popular.g.dart @@ -58,7 +58,7 @@ final class GetPopularProvider } } -String _$getPopularHash() => r'f169b6a9ba76d9dd9237ba9c21805151a1419843'; +String _$getPopularHash() => r'7e1139bc0f6a3a495fa0dc59d450bc7fd70f36a8'; final class GetPopularFamily extends $Family with diff --git a/lib/services/get_video_list.dart b/lib/services/get_video_list.dart index 0b0cb81b..784c8d15 100644 --- a/lib/services/get_video_list.dart +++ b/lib/services/get_video_list.dart @@ -86,7 +86,7 @@ Future<(List