diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7900e6b..b7752a2 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -148,6 +148,45 @@ jobs: sudo rm -rf build/linux/arch/pkg sudo rm -rf build/linux/arch/src sudo rm -rf build/linux/arch/PKGBUILD + - name: Build AppImage + run: | + sudo apt-get install -y libfuse2 + wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" + chmod +x appimagetool + + mkdir -p Venera.AppDir + cp -r build/linux/x64/release/bundle/* Venera.AppDir/ + + cat > Venera.AppDir/venera.desktop << EOF + [Desktop Entry] + Name=Venera + Exec=venera + Icon=venera + Type=Application + Categories=Utility; + EOF + + cp assets/app_icon.png Venera.AppDir/venera.png + + cat > Venera.AppDir/AppRun << EOF + #!/bin/sh + HERE=\$(dirname \$(readlink -f "\${0}")) + export PATH="\${HERE}"/usr/bin/:"\${HERE}"/usr/sbin/:"\${HERE}"/usr/games/:"\${HERE}"/bin/:"\${HERE}"/sbin/:\${PATH} + export LD_LIBRARY_PATH="\${HERE}"/usr/lib/:\${LD_LIBRARY_PATH} + export XDG_DATA_DIRS="\${HERE}"/usr/share/:\${XDG_DATA_DIRS} + exec "\${HERE}"/venera "\$@" + EOF + chmod +x Venera.AppDir/AppRun + + APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ') + ./appimagetool Venera.AppDir Venera-${APP_VERSION}-x86_64.AppImage + + mkdir -p build/linux/appimage + mv Venera-${APP_VERSION}-x86_64.AppImage build/linux/appimage/ + - uses: actions/upload-artifact@v4 + with: + name: appimage_build + path: build/linux/appimage - uses: actions/upload-artifact@v4 with: name: deb_build @@ -170,6 +209,45 @@ jobs: sudo apt-get install -y ninja-build libgtk-3-dev webkit2gtk-4.1 dart pub global activate flutter_to_debian - run: python3 debian/build.py arm64 + - name: Build AppImage + run: | + sudo apt-get install -y libfuse2 + wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-aarch64.AppImage" + chmod +x appimagetool + + mkdir -p Venera.AppDir + cp -r build/linux/arm64/release/bundle/* Venera.AppDir/ + + cat > Venera.AppDir/venera.desktop << EOF + [Desktop Entry] + Name=Venera + Exec=venera + Icon=venera + Type=Application + Categories=Utility; + EOF + + cp assets/app_icon.png Venera.AppDir/venera.png + + cat > Venera.AppDir/AppRun << EOF + #!/bin/sh + HERE=\$(dirname \$(readlink -f "\${0}")) + export PATH="\${HERE}"/usr/bin/:"\${HERE}"/usr/sbin/:"\${HERE}"/usr/games/:"\${HERE}"/bin/:"\${HERE}"/sbin/:\${PATH} + export LD_LIBRARY_PATH="\${HERE}"/usr/lib/:\${LD_LIBRARY_PATH} + export XDG_DATA_DIRS="\${HERE}"/usr/share/:\${XDG_DATA_DIRS} + exec "\${HERE}"/venera "\$@" + EOF + chmod +x Venera.AppDir/AppRun + + APP_VERSION=$(grep "version:" pubspec.yaml | cut -d':' -f2 | tr -d ' ') + ./appimagetool Venera.AppDir Venera-${APP_VERSION}-aarch64.AppImage + + mkdir -p build/linux/appimage + mv Venera-${APP_VERSION}-aarch64.AppImage build/linux/appimage/ + - uses: actions/upload-artifact@v4 + with: + name: appimage_arm64_build + path: build/linux/appimage - uses: actions/upload-artifact@v4 with: name: deb_arm64_build @@ -208,6 +286,14 @@ jobs: with: name: deb_arm64_build path: outputs + - uses: actions/download-artifact@v4 + with: + name: appimage_build + path: outputs + - uses: actions/download-artifact@v4 + with: + name: appimage_arm64_build + path: outputs - uses: softprops/action-gh-release@v2 with: tag_name: ${{ github.ref_name }} @@ -219,5 +305,6 @@ jobs: outputs/*.exe outputs/*.deb outputs/*.zst + outputs/*.AppImage env: GITHUB_TOKEN: ${{ secrets.ACTION_GITHUB_TOKEN }} diff --git a/assets/translation.json b/assets/translation.json index fda50e2..33f9aa7 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -106,7 +106,8 @@ "Continuous (Right to Left)": "连续(从右到左)", "Continuous (Top to Bottom)": "连续(从上到下)", "Auto page turning interval": "自动翻页间隔", - "The number of pic in screen (Only Gallery Mode)": "同屏幕图片数量(仅画廊模式)", + "The number of pic in screen for landscape (Only Gallery Mode)": "横屏同屏幕图片数量(仅画廊模式)", + "The number of pic in screen for portrait (Only Gallery Mode)": "竖屏同屏幕图片数量(仅画廊模式)", "Theme Mode": "主题模式", "System": "系统", "Light": "浅色", @@ -189,6 +190,7 @@ "Operation": "操作", "Upload": "上传", "Saved": "已保存", + "Saved Failed": "保存失败", "Sync Data": "同步数据", "Syncing Data": "正在同步数据", "Data Sync": "数据同步", @@ -336,8 +338,7 @@ "Number of images preloaded": "预加载图片数量", "Ascending": "升序", "Descending": "降序", - "Last Reading: Chapter @ep Page @page": "上次阅读: 第 @ep 章 第 @page 页", - "Last Reading: Page @page": "上次阅读: 第 @page 页", + "Last Reading": "上次阅读", "Replies": "回复", "Follow Updates": "追更", "Not Configured": "未配置", @@ -353,7 +354,15 @@ "No updates found": "未找到更新", "All Comics": "全部漫画", "The comic will be marked as no updates as soon as you read it.": "漫画将在您阅读后立即标记为无更新", - "Disable": "禁用" + "Disable": "禁用", + "Once the operation is successful, app will automatically sync data with the server.": "操作成功后, APP将自动与服务器同步数据", + "Cache cleared": "缓存已清除", + "Disabled": "已禁用", + "WebDAV Auto Sync": "WebDAV 自动同步", + "Mark all as read": "全部标记为已读", + "Do you want to mark all as read?" : "您要全部标记为已读吗?", + "Swipe down for previous chapter": "向下滑动查看上一章", + "Swipe up for next chapter": "向上滑动查看下一章" }, "zh_TW": { "Home": "首頁", @@ -461,7 +470,8 @@ "Continuous (Right to Left)": "連續(從右到左)", "Continuous (Top to Bottom)": "連續(從上到下)", "Auto page turning interval": "自動翻頁間隔", - "The number of pic in screen (Only Gallery Mode)": "同螢幕圖片數量(僅畫廊模式)", + "The number of pic in screen for landscape (Only Gallery Mode)": "橫向同螢幕圖片數量(僅畫廊模式)", + "The number of pic in screen for portrait (Only Gallery Mode)": "直向同螢幕圖片數量(僅畫廊模式)", "Theme Mode": "主題模式", "System": "系統", "Light": "淺色", @@ -545,6 +555,7 @@ "Operation": "操作", "Upload": "上傳", "Saved": "已儲存", + "Saved Failed": "儲存失敗", "Sync Data": "同步資料", "Syncing Data": "正在同步資料", "Data Sync": "資料同步", @@ -692,8 +703,7 @@ "Number of images preloaded": "預載入圖片數量", "Ascending": "升序", "Descending": "降序", - "Last Reading: Chapter @ep Page @page": "上次閱讀: 第 @ep 章 第 @page 頁", - "Last Reading: Page @page": "上次閱讀: 第 @page 頁", + "Last Reading": "上次閱讀", "Replies": "回覆", "Follow Updates": "追更", "Not Configured": "未配置", @@ -709,6 +719,14 @@ "No updates found": "未找到更新", "All Comics": "全部漫畫", "The comic will be marked as no updates as soon as you read it.": "漫畫將在您閱讀後立即標記為無更新", - "Disable": "停用" + "Disable": "停用", + "Once the operation is successful, app will automatically sync data with the server.": "操作成功後, APP將自動與服務器同步數據", + "Cache cleared": "緩存已清除", + "Disabled": "已禁用", + "WebDAV Auto Sync": "WebDAV 自動同步", + "Mark all as read": "全部標記為已讀", + "Do you want to mark all as read?" : "您要全部標記為已讀嗎?", + "Swipe down for previous chapter": "向下滑動查看上一章", + "Swipe up for next chapter": "向上滑動查看下一章" } } diff --git a/lib/components/appbar.dart b/lib/components/appbar.dart index c29d696..5be9e44 100644 --- a/lib/components/appbar.dart +++ b/lib/components/appbar.dart @@ -632,6 +632,7 @@ class _TabViewBodyState extends State { void didChangeDependencies() { super.didChangeDependencies(); _controller = widget.controller ?? DefaultTabController.of(context); + _currentIndex = _controller.index; _controller.addListener(updateIndex); } diff --git a/lib/foundation/app.dart b/lib/foundation/app.dart index 91b6509..7415bbc 100644 --- a/lib/foundation/app.dart +++ b/lib/foundation/app.dart @@ -3,14 +3,17 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:venera/foundation/history.dart'; import 'appdata.dart'; +import 'favorites.dart'; +import 'local.dart'; export "widget_utils.dart"; export "context.dart"; class _App { - final version = "1.3.0"; + final version = "1.3.1"; bool get isAndroid => Platform.isAndroid; @@ -51,6 +54,14 @@ class _App { BuildContext get rootContext => rootNavigatorKey.currentContext!; + final Appdata data = appdata; + + final HistoryManager history = HistoryManager(); + + final LocalFavoritesManager favorites = LocalFavoritesManager(); + + final LocalManager local = LocalManager(); + void rootPop() { rootNavigatorKey.currentState?.maybePop(); } @@ -66,6 +77,10 @@ class _App { Future init() async { cachePath = (await getApplicationCacheDirectory()).path; dataPath = (await getApplicationSupportDirectory()).path; + await data.init(); + await history.init(); + await favorites.init(); + await local.init(); } Function? _forceRebuildHandler; diff --git a/lib/foundation/appdata.dart b/lib/foundation/appdata.dart index e6c1dcc..567cdda 100644 --- a/lib/foundation/appdata.dart +++ b/lib/foundation/appdata.dart @@ -6,8 +6,8 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/io.dart'; -class _Appdata { - final _Settings settings = _Settings(); +class Appdata { + final Settings settings = Settings(); var searchHistory = []; @@ -110,10 +110,10 @@ class _Appdata { } } -final appdata = _Appdata(); +final appdata = Appdata(); -class _Settings with ChangeNotifier { - _Settings(); +class Settings with ChangeNotifier { + Settings(); final _data = { 'comicDisplayMode': 'detailed', // detailed, brief @@ -133,7 +133,8 @@ class _Settings with ChangeNotifier { 'defaultSearchTarget': null, 'autoPageTurningInterval': 5, // in seconds 'readerMode': 'galleryLeftToRight', // values of [ReaderMode] - 'readerScreenPicNumber': 1, // 1 - 5 + 'readerScreenPicNumberForLandscape': 1, // 1 - 5 + 'readerScreenPicNumberForPortrait': 1, // 1 - 5 'enableTapToTurnPages': true, 'reverseTapToTurnPages': false, 'enablePageAnimation': true, @@ -188,9 +189,9 @@ const defaultCustomImageProcessing = ''' * @returns {Promise | {image: Promise, onCancel: () => void}} - The processed image */ function processImage(image, cid, eid, page, sourceKey) { - let image = new Promise((resolve, reject) => { + let futureImage = new Promise((resolve, reject) => { resolve(image); }); - return image; + return futureImage; } '''; diff --git a/lib/foundation/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 8e39b46..c80fea0 100644 --- a/lib/foundation/comic_source/models.dart +++ b/lib/foundation/comic_source/models.dart @@ -128,12 +128,7 @@ class ComicDetails with HistoryMixin { final Map> tags; /// id-name - final Map? chapters; - - /// key is group name. - /// When this field is not null, [chapters] will be a merged map of all groups. - /// Only available in some sources. - final Map>? groupedChapters; + final ComicChapters? chapters; final List? thumbnails; @@ -176,45 +171,13 @@ class ComicDetails with HistoryMixin { return res; } - static Map? _getChapters(dynamic chapters) { - if (chapters == null) return null; - var result = {}; - if (chapters is Map) { - for (var entry in chapters.entries) { - var value = entry.value; - if (value is Map) { - result.addAll(Map.from(value)); - } else { - result[entry.key.toString()] = value.toString(); - } - } - } - return result; - } - - static Map>? _getGroupedChapters(dynamic chapters) { - if (chapters == null) return null; - var result = >{}; - if (chapters is Map) { - for (var entry in chapters.entries) { - var value = entry.value; - if (value is Map) { - result[entry.key.toString()] = Map.from(value); - } - } - } - if (result.isEmpty) return null; - return result; - } - ComicDetails.fromJson(Map json) : title = json["title"], subTitle = json["subtitle"], cover = json["cover"], description = json["description"], tags = _generateMap(json["tags"]), - chapters = _getChapters(json["chapters"]), - groupedChapters = _getGroupedChapters(json["chapters"]), + chapters = ComicChapters.fromJsonOrNull(json["chapters"]), sourceKey = json["sourceKey"], comicId = json["comicId"], thumbnails = ListOrNull.from(json["thumbnails"]), @@ -342,3 +305,122 @@ class ArchiveInfo { description = json["description"], id = json["id"]; } + +class ComicChapters { + final Map? _chapters; + + final Map>? _groupedChapters; + + /// Create a ComicChapters object with a flat map + const ComicChapters(Map this._chapters) + : _groupedChapters = null; + + /// Create a ComicChapters object with a grouped map + const ComicChapters.grouped( + Map> this._groupedChapters) + : _chapters = null; + + factory ComicChapters.fromJson(dynamic json) { + if (json is! Map) throw ArgumentError("Invalid json type"); + var chapters = {}; + var groupedChapters = >{}; + for (var entry in json.entries) { + var key = entry.key; + var value = entry.value; + if (key is! String) throw ArgumentError("Invalid key type"); + if (value is Map) { + groupedChapters[key] = Map.from(value); + } else { + chapters[key] = value.toString(); + } + } + if (chapters.isNotEmpty) { + return ComicChapters(chapters); + } else { + return ComicChapters.grouped(groupedChapters); + } + } + + static fromJsonOrNull(dynamic json) { + if (json == null) return null; + return ComicChapters.fromJson(json); + } + + Map toJson() { + if (_chapters != null) { + return _chapters; + } else { + return _groupedChapters!; + } + } + + /// Whether the chapters are grouped + bool get isGrouped => _groupedChapters != null; + + /// All group names + Iterable get groups => _groupedChapters?.keys ?? []; + + /// All chapters. + /// If the chapters are grouped, all groups will be merged. + Map get allChapters { + if (_chapters != null) return _chapters; + var res = {}; + for (var entry in _groupedChapters!.values) { + res.addAll(entry); + } + return res; + } + + /// Get a group of chapters by name + Map getGroup(String group) { + return _groupedChapters![group] ?? {}; + } + + /// Get a group of chapters by index(0-based) + Map getGroupByIndex(int index) { + return _groupedChapters!.values.elementAt(index); + } + + /// Get total number of chapters + int get length { + return isGrouped + ? _groupedChapters!.values.map((e) => e.length).reduce((a, b) => a + b) + : _chapters!.length; + } + + /// Get the number of groups + int get groupCount => _groupedChapters?.length ?? 0; + + /// Iterate all chapter ids + Iterable get ids sync* { + if (isGrouped) { + for (var entry in _groupedChapters!.values) { + yield* entry.keys; + } + } else { + yield* _chapters!.keys; + } + } + + /// Iterate all chapter titles + Iterable get titles sync* { + if (isGrouped) { + for (var entry in _groupedChapters!.values) { + yield* entry.values; + } + } else { + yield* _chapters!.values; + } + } + + String? operator [](String key) { + if (isGrouped) { + for (var entry in _groupedChapters!.values) { + if (entry.containsKey(key)) return entry[key]; + } + return null; + } else { + return _chapters![key]; + } + } +} diff --git a/lib/foundation/favorites.dart b/lib/foundation/favorites.dart index 422ddad..f7ac7a4 100644 --- a/lib/foundation/favorites.dart +++ b/lib/foundation/favorites.dart @@ -185,6 +185,18 @@ class FavoriteItemWithUpdateInfo extends FavoriteItem { var sourceName = type.comicSource?.name ?? "Unknown"; return "$updateTime | $sourceName"; } + + @override + operator ==(Object other) { + return other is FavoriteItemWithUpdateInfo && + other.updateTime == updateTime && + other.hasNewUpdate == hasNewUpdate && + super == other; + } + + @override + int get hashCode => + super.hashCode ^ updateTime.hashCode ^ hasNewUpdate.hashCode; } class LocalFavoritesManager with ChangeNotifier { @@ -785,7 +797,7 @@ class LocalFavoritesManager with ChangeNotifier { } } - void updateInfo(String folder, FavoriteItem comic) { + void updateInfo(String folder, FavoriteItem comic, [bool notify = true]) { _db.execute(""" update "$folder" set name = ?, author = ?, cover_path = ?, tags = ? @@ -798,7 +810,9 @@ class LocalFavoritesManager with ChangeNotifier { comic.id, comic.type.value ]); - notifyListeners(); + if (notify) { + notifyListeners(); + } } String folderToJson(String folder) { @@ -888,6 +902,18 @@ class LocalFavoritesManager with ChangeNotifier { ]); } + void updateCheckTime( + String folder, + String id, + ComicType type, + ) { + _db.execute(""" + update "$folder" + set last_check_time = ? + where id == ? and type == ?; + """, [DateTime.now().millisecondsSinceEpoch, id, type.value]); + } + int countUpdates(String folder) { return _db.select(""" select count(*) as c from "$folder" @@ -949,4 +975,8 @@ class LocalFavoritesManager with ChangeNotifier { void close() { _db.dispose(); } + + void notifyChanges() { + notifyListeners(); + } } diff --git a/lib/foundation/history.dart b/lib/foundation/history.dart index bd21d1d..0c017cf 100644 --- a/lib/foundation/history.dart +++ b/lib/foundation/history.dart @@ -7,7 +7,6 @@ import 'dart:ffi' as ffi; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart' show ChangeNotifier; -import 'package:sqlite3/common.dart'; import 'package:sqlite3/sqlite3.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; @@ -51,17 +50,24 @@ class History implements Comic { @override String cover; + /// index of chapters. 1-based. int ep; + /// index of pages. 1-based. int page; + /// index of chapter groups. 1-based. + /// If [group] is not null, [ep] is the index of chapter in the group. + int? group; + @override String id; /// readEpisode is a set of episode numbers that have been read. - /// - /// The number of episodes is 1-based. - Set readEpisode; + /// For normal chapters, it is a set of chapter numbers. + /// For grouped chapters, it is a set of strings in the format of "group_number-chapter_number". + /// 1-based. + Set readEpisode; @override int? maxPage; @@ -70,29 +76,17 @@ class History implements Comic { {required HistoryMixin model, required this.ep, required this.page, - Set? readChapters, + this.group, + Set? readChapters, DateTime? time}) : type = model.historyType, title = model.title, subtitle = model.subTitle ?? '', cover = model.cover, id = model.id, - readEpisode = readChapters ?? {}, + readEpisode = readChapters ?? {}, time = time ?? DateTime.now(); - Map toMap() => { - "type": type.value, - "time": time.millisecondsSinceEpoch, - "title": title, - "subtitle": subtitle, - "cover": cover, - "ep": ep, - "page": page, - "id": id, - "readEpisode": readEpisode.toList(), - "max_page": maxPage - }; - History.fromMap(Map map) : type = HistoryType(map["type"]), time = DateTime.fromMillisecondsSinceEpoch(map["time"]), @@ -102,8 +96,9 @@ class History implements Comic { ep = map["ep"], page = map["page"], id = map["id"], - readEpisode = Set.from( - (map["readEpisode"] as List?)?.toSet() ?? const {}), + readEpisode = Set.from( + (map["readEpisode"] as List?)?.toSet() ?? + const {}), maxPage = map["max_page"]; @override @@ -120,11 +115,11 @@ class History implements Comic { ep = row["ep"], page = row["page"], id = row["id"], - readEpisode = Set.from((row["readEpisode"] as String) + readEpisode = Set.from((row["readEpisode"] as String) .split(',') - .where((element) => element != "") - .map((e) => int.parse(e))), - maxPage = row["max_page"]; + .where((element) => element != "")), + maxPage = row["max_page"], + group = row["chapter_group"]; @override bool operator ==(Object other) { @@ -213,18 +208,24 @@ class HistoryManager with ChangeNotifier { ep int, page int, readEpisode text, - max_page int + max_page int, + chapter_group int ); """); + var columns = _db.select("PRAGMA table_info(history);"); + if (!columns.any((element) => element["name"] == "chapter_group")) { + _db.execute("alter table history add column chapter_group int;"); + } + notifyListeners(); ImageFavoriteManager().init(); isInitialized = true; } - static const _insertHistorySql = """ - insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page) - values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + static const _insertHistorySql = """ + insert or replace into history (id, title, subtitle, cover, time, type, ep, page, readEpisode, max_page, chapter_group) + values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); """; static Future _addHistoryAsync(int dbAddr, History newItem) { @@ -240,7 +241,8 @@ class HistoryManager with ChangeNotifier { newItem.ep, newItem.page, newItem.readEpisode.join(','), - newItem.maxPage + newItem.maxPage, + newItem.group ]); }); } @@ -282,7 +284,8 @@ class HistoryManager with ChangeNotifier { newItem.ep, newItem.page, newItem.readEpisode.join(','), - newItem.maxPage + newItem.maxPage, + newItem.group ]); if (_cachedHistoryIds == null) { updateCache(); @@ -319,7 +322,7 @@ class HistoryManager with ChangeNotifier { for (var element in res) { _cachedHistoryIds![element["id"] as String] = true; } - for (var key in cachedHistories.keys) { + for (var key in cachedHistories.keys.toList()) { if (!_cachedHistoryIds!.containsKey(key)) { cachedHistories.remove(key); } diff --git a/lib/foundation/image_provider/image_favorites_provider.dart b/lib/foundation/image_provider/image_favorites_provider.dart index 0ccea9d..b983c93 100644 --- a/lib/foundation/image_provider/image_favorites_provider.dart +++ b/lib/foundation/image_provider/image_favorites_provider.dart @@ -97,7 +97,7 @@ class ImageFavoritesProvider if (localComic == null) { return null; } - var epIndex = localComic.chapters?.keys.toList().indexOf(eid) ?? -1; + var epIndex = localComic.chapters?.ids.toList().indexOf(eid) ?? -1; if (epIndex == -1 && localComic.hasChapters) { return null; } diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 8cafdfc..c2b462c 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -9,7 +9,6 @@ import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/download.dart'; import 'package:venera/pages/reader/reader.dart'; -import 'package:venera/utils/ext.dart'; import 'package:venera/utils/io.dart'; import 'app.dart'; @@ -34,7 +33,7 @@ class LocalComic with HistoryMixin implements Comic { /// key: chapter id, value: chapter title /// /// chapter id is the name of the directory in `LocalManager.path/$directory` - final Map? chapters; + final ComicChapters? chapters; bool get hasChapters => chapters != null; @@ -67,7 +66,7 @@ class LocalComic with HistoryMixin implements Comic { subtitle = row[2] as String, tags = List.from(jsonDecode(row[3] as String)), directory = row[4] as String, - chapters = MapOrNull.from(jsonDecode(row[5] as String)), + chapters = ComicChapters.fromJsonOrNull(jsonDecode(row[5] as String)), cover = row[6] as String, comicType = ComicType(row[7] as int), downloadedChapters = List.from(jsonDecode(row[8] as String)), @@ -99,6 +98,7 @@ class LocalComic with HistoryMixin implements Comic { "tags": tags, "description": description, "sourceKey": sourceKey, + "chapters": chapters?.toJson(), }; } @@ -115,6 +115,7 @@ class LocalComic with HistoryMixin implements Comic { chapters: chapters, initialChapter: history?.ep, initialPage: history?.page, + initialChapterGroup: history?.group, history: history ?? History.fromModel( model: this, @@ -391,7 +392,7 @@ class LocalManager with ChangeNotifier { var directory = Directory(comic.baseDir); if (comic.hasChapters) { var cid = - ep is int ? comic.chapters!.keys.elementAt(ep - 1) : (ep as String); + ep is int ? comic.chapters!.ids.elementAt(ep - 1) : (ep as String); directory = Directory(FilePath.join(directory.path, cid)); } var files = []; @@ -425,7 +426,7 @@ class LocalManager with ChangeNotifier { if (comic == null) return false; if (comic.chapters == null || ep == null) return true; return comic.downloadedChapters - .contains(comic.chapters!.keys.elementAt(ep - 1)); + .contains(comic.chapters!.ids.elementAt(ep - 1)); } List downloadingTasks = []; @@ -509,7 +510,7 @@ class LocalManager with ChangeNotifier { var dir = Directory(FilePath.join(path, c.directory)); dir.deleteIgnoreError(recursive: true); } - // Deleting a local comic means that it's nolonger available, thus both favorite and history should be deleted. + // Deleting a local comic means that it's no longer available, thus both favorite and history should be deleted. if (c.comicType == ComicType.local) { if (HistoryManager().find(c.id, c.comicType) != null) { HistoryManager().remove(c.id, c.comicType); diff --git a/lib/init.dart b/lib/init.dart index 972d857..8435771 100644 --- a/lib/init.dart +++ b/lib/init.dart @@ -4,10 +4,7 @@ import 'package:rhttp/rhttp.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/cache_manager.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; -import 'package:venera/foundation/favorites.dart'; -import 'package:venera/foundation/history.dart'; import 'package:venera/foundation/js_engine.dart'; -import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/network/cookie_jar.dart'; import 'package:venera/pages/comic_source_page.dart'; @@ -32,25 +29,18 @@ extension _FutureInit on Future { } Future init() async { - await Rhttp.init(); - await SAFTaskWorker().init().wait(); - await AppTranslation.init().wait(); - await appdata.init().wait(); await App.init().wait(); - await HistoryManager().init().wait(); - await TagsTranslation.readData().wait(); - await LocalFavoritesManager().init().wait(); SingleInstanceCookieJar("${App.dataPath}/cookie.db"); - await JsEngine().init().wait(); - await ComicSource.init().wait(); - await LocalManager().init().wait(); + var futures = [ + Rhttp.init(), + SAFTaskWorker().init().wait(), + AppTranslation.init().wait(), + TagsTranslation.readData().wait(), + JsEngine().init().then((_) => ComicSource.init()).wait(), + ]; + await Future.wait(futures); CacheManager().setLimitSize(appdata.settings['cacheSize']); - if (appdata.settings['searchSources'] == null) { - appdata.settings['searchSources'] = ComicSource.all() - .where((e) => e.searchPageData != null) - .map((e) => e.key) - .toList(); - } + _checkOldConfigs(); if (App.isAndroid) { handleLinks(); } @@ -59,6 +49,27 @@ Future init() async { }; } +void _checkOldConfigs() { + if (appdata.settings['searchSources'] == null) { + appdata.settings['searchSources'] = ComicSource.all() + .where((e) => e.searchPageData != null) + .map((e) => e.key) + .toList(); + } + + if (appdata.implicitData['webdavAutoSync'] == null) { + var webdavConfig = appdata.settings['webdav']; + if (webdavConfig is List && + webdavConfig.length == 3 && + webdavConfig.whereType().length == 3) { + appdata.implicitData['webdavAutoSync'] = true; + } else { + appdata.implicitData['webdavAutoSync'] = false; + } + appdata.writeImplicitData(); + } +} + Future _checkAppUpdates() async { var lastCheck = appdata.implicitData['lastCheckUpdate'] ?? 0; var now = DateTime.now().millisecondsSinceEpoch; diff --git a/lib/network/cloudflare.dart b/lib/network/cloudflare.dart index 4c078e2..8b51fe8 100644 --- a/lib/network/cloudflare.dart +++ b/lib/network/cloudflare.dart @@ -14,7 +14,7 @@ import 'cookie_jar.dart'; class CloudflareException implements DioException { final String url; - const CloudflareException(this.url); + CloudflareException(this.url); @override String toString() { @@ -55,6 +55,9 @@ class CloudflareException implements DioException { @override DioExceptionType get type => DioExceptionType.badResponse; + + @override + DioExceptionReadableStringBuilder? stringBuilder; } class CloudflareInterceptor extends Interceptor { diff --git a/lib/network/download.dart b/lib/network/download.dart index 058b93e..39d77c0 100644 --- a/lib/network/download.dart +++ b/lib/network/download.dart @@ -328,8 +328,9 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { _images = {}; _totalCount = 0; int cpCount = 0; - int totalCpCount = chapters?.length ?? comic!.chapters!.length; - for (var i in comic!.chapters!.keys) { + int totalCpCount = + chapters?.length ?? comic!.chapters!.allChapters.length; + for (var i in comic!.chapters!.allChapters.keys) { if (chapters != null && !chapters!.contains(i)) { continue; } @@ -422,7 +423,7 @@ class ImagesDownloadTask extends DownloadTask with _TransferSpeedMixin { "comic": comic?.toJson(), "chapters": chapters, "path": path, - "cover": cover, + "cover": _cover, "images": _images, "downloadedCount": _downloadedCount, "totalCount": _totalCount, diff --git a/lib/network/images.dart b/lib/network/images.dart index 6a5c9a1..f16e0ea 100644 --- a/lib/network/images.dart +++ b/lib/network/images.dart @@ -139,12 +139,10 @@ class ImageDownloader { var buffer = []; await for (var data in stream) { buffer.addAll(data); - if (expectedBytes != null) { - yield ImageDownloadProgress( - currentBytes: buffer.length, - totalBytes: expectedBytes, - ); - } + yield ImageDownloadProgress( + currentBytes: buffer.length, + totalBytes: expectedBytes, + ); } if (configs['onResponse'] is JSInvokable) { @@ -194,7 +192,7 @@ class ImageDownloader { class ImageDownloadProgress { final int currentBytes; - final int totalBytes; + final int? totalBytes; final Uint8List? imageBytes; diff --git a/lib/pages/comic_details_page/actions.dart b/lib/pages/comic_details_page/actions.dart index 7636426..90f0a35 100644 --- a/lib/pages/comic_details_page/actions.dart +++ b/lib/pages/comic_details_page/actions.dart @@ -95,16 +95,19 @@ abstract mixin class _ComicPageActions { /// [ep] the episode number, start from 1 /// /// [page] the page number, start from 1 - void read([int? ep, int? page]) { + /// + /// [group] the chapter group number, start from 1 + void read([int? ep, int? page, int? group]) { App.rootContext .to( - () => Reader( + () => Reader( type: comic.comicType, cid: comic.id, name: comic.title, chapters: comic.chapters, initialChapter: ep, initialPage: page, + initialChapterGroup: group, history: history ?? History.fromModel(model: comic, ep: 0, page: 0), author: comic.findAuthor() ?? '', tags: comic.plainTags, @@ -118,7 +121,8 @@ abstract mixin class _ComicPageActions { void continueRead() { var ep = history?.ep ?? 1; var page = history?.page ?? 1; - read(ep, page); + var group = history?.group ?? 1; + read(ep, page, group); } void onReadEnd(); @@ -219,7 +223,7 @@ abstract mixin class _ComicPageActions { isGettingLink = true; }); var res = - await comicSource.archiveDownloader!.getDownloadUrl( + await comicSource.archiveDownloader!.getDownloadUrl( comic.id, archives![selected].id, ); @@ -262,7 +266,7 @@ abstract mixin class _ComicPageActions { if (localComic != null) { for (int i = 0; i < comic.chapters!.length; i++) { if (localComic.downloadedChapters - .contains(comic.chapters!.keys.elementAt(i))) { + .contains(comic.chapters!.ids.elementAt(i))) { downloaded.add(i); } } @@ -270,8 +274,8 @@ abstract mixin class _ComicPageActions { await showSideBar( App.rootContext, _SelectDownloadChapter( - comic.chapters!.values.toList(), - (v) => selected = v, + comic.chapters!.titles.toList(), + (v) => selected = v, downloaded, ), ); @@ -281,7 +285,7 @@ abstract mixin class _ComicPageActions { comicId: comic.id, comic: comic, chapters: selected!.map((i) { - return comic.chapters!.keys.elementAt(i); + return comic.chapters!.ids.elementAt(i); }).toList(), )); } @@ -298,13 +302,13 @@ abstract mixin class _ComicPageActions { var context = App.mainNavigatorKey!.currentContext!; if (config['action'] == 'search') { context.to(() => SearchResultPage( - text: config['keyword'] ?? '', - sourceKey: comicSource.key, - options: const [], - )); + text: config['keyword'] ?? '', + sourceKey: comicSource.key, + options: const [], + )); } else if (config['action'] == 'category') { context.to( - () => CategoryComicsPage( + () => CategoryComicsPage( category: config['keyword'] ?? '', categoryKey: comicSource.categoryData!.key, param: config['param'], @@ -432,4 +436,4 @@ abstract mixin class _ComicPageActions { ), ); } -} \ No newline at end of file +} diff --git a/lib/pages/comic_details_page/chapters.dart b/lib/pages/comic_details_page/chapters.dart index 364871c..0a9586f 100644 --- a/lib/pages/comic_details_page/chapters.dart +++ b/lib/pages/comic_details_page/chapters.dart @@ -33,7 +33,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> { late History? history; - late Map chapters; + late ComicChapters chapters; @override void initState() { @@ -101,7 +101,7 @@ class _NormalComicChaptersState extends State<_NormalComicChapters> { if (reverse) { i = chapters.length - i - 1; } - var key = chapters.keys.elementAt(i); + var key = chapters.ids.elementAt(i); var value = chapters[key]!; bool visited = (history?.readEpisode ?? {}).contains(i + 1); return Padding( @@ -182,7 +182,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> late History? history; - late Map> chapters; + late ComicChapters chapters; late TabController tabController; @@ -197,9 +197,9 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> @override void didChangeDependencies() { state = context.findAncestorStateOfType<_ComicPageState>()!; - chapters = state.comic.groupedChapters!; + chapters = state.comic.chapters!; tabController = TabController( - length: chapters.keys.length, + length: chapters.ids.length, vsync: this, ); tabController.addListener(onTabChange); @@ -226,7 +226,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> Widget build(BuildContext context) { return SliverLayoutBuilder( builder: (context, constrains) { - var group = chapters.values.elementAt(index); + var group = chapters.getGroupByIndex(index); int length = group.length; bool canShowAll = showAll; if (!showAll) { @@ -265,7 +265,7 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> child: AppTabBar( withUnderLine: false, controller: tabController, - tabs: chapters.keys.map((e) => Tab(text: e)).toList(), + tabs: chapters.groups.map((e) => Tab(text: e)).toList(), ), ), SliverPadding(padding: const EdgeInsets.only(top: 8)), @@ -279,15 +279,20 @@ class _GroupedComicChaptersState extends State<_GroupedComicChapters> var key = group.keys.elementAt(i); var value = group[key]!; var chapterIndex = 0; - for (var j = 0; j < chapters.length; j++) { + for (var j = 0; j < chapters.groupCount; j++) { if (j == index) { chapterIndex += i; break; } - chapterIndex += chapters.values.elementAt(j).length; + chapterIndex += chapters.getGroupByIndex(j).length; + } + String rawIndex = (chapterIndex + 1).toString(); + String groupedIndex = "${index + 1}-${i + 1}"; + bool visited = false; + if (history != null) { + visited = history!.readEpisode.contains(groupedIndex) || + history!.readEpisode.contains(rawIndex); } - bool visited = - (history?.readEpisode ?? {}).contains(chapterIndex + 1); return Padding( padding: const EdgeInsets.fromLTRB(6, 4, 6, 4), child: Material( diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index 6cf8913..5447a31 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -165,6 +165,9 @@ class _ComicPageState extends LoadingState cid: widget.id, name: localComic.title, chapters: localComic.chapters, + initialPage: history?.page, + initialChapter: history?.ep, + initialChapterGroup: history?.group, history: history ?? History.fromModel( model: localComic, @@ -369,7 +372,7 @@ class _ComicPageState extends LoadingState padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), decoration: BoxDecoration( color: context.colorScheme.surfaceContainerLow, - borderRadius: BorderRadius.circular(16), + borderRadius: BorderRadius.circular(24), ), child: Row( mainAxisSize: MainAxisSize.min, @@ -381,16 +384,20 @@ class _ComicPageState extends LoadingState bool haveChapter = comic.chapters != null; var page = history!.page; var ep = history!.ep; + var group = history!.group; String text; if (haveChapter) { - text = "Last Reading: Chapter @ep Page @page".tlParams({ - 'ep': ep, - 'page': page, - }); + var epName = group == null + ? comic.chapters!.titles.elementAt( + math.min(ep - 1, comic.chapters!.length - 1), + ) + : comic.chapters! + .getGroupByIndex(group - 1) + .values + .elementAt(ep - 1); + text = "${"Last Reading".tl}: $epName P$page"; } else { - text = "Last Reading: Page @page".tlParams({ - 'page': page, - }); + text = "${"Last Reading".tl}: P$page"; } return Text(text); }, @@ -607,7 +614,7 @@ class _ComicPageState extends LoadingState } return _ComicChapters( history: history, - groupedMode: comic.groupedChapters != null, + groupedMode: comic.chapters!.isGrouped, ); } diff --git a/lib/pages/downloading_page.dart b/lib/pages/downloading_page.dart index 795d6e0..1ec15c6 100644 --- a/lib/pages/downloading_page.dart +++ b/lib/pages/downloading_page.dart @@ -15,6 +15,15 @@ class DownloadingPage extends StatefulWidget { } class _DownloadingPageState extends State { + DownloadTask? firstTask; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + firstTask = LocalManager().downloadingTasks.firstOrNull; + firstTask?.addListener(update); + } + @override void initState() { LocalManager().addListener(update); @@ -24,10 +33,17 @@ class _DownloadingPageState extends State { @override void dispose() { LocalManager().removeListener(update); + firstTask?.removeListener(update); super.dispose(); } void update() { + var currentFirstTask = LocalManager().downloadingTasks.firstOrNull; + if (currentFirstTask != firstTask) { + firstTask?.removeListener(update); + firstTask = currentFirstTask; + firstTask?.addListener(update); + } if(mounted) { setState(() {}); } diff --git a/lib/pages/follow_updates_page.dart b/lib/pages/follow_updates_page.dart index 45fa8a5..7b56868 100644 --- a/lib/pages/follow_updates_page.dart +++ b/lib/pages/follow_updates_page.dart @@ -6,6 +6,7 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/log.dart'; +import 'package:venera/utils/data_sync.dart'; import 'package:venera/utils/translations.dart'; import '../foundation/global_state.dart'; @@ -133,7 +134,18 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { } else if (b.updateTime == null) { return 1; } - return b.updateTime!.compareTo(a.updateTime!); + try { + var aNums = a.updateTime!.split('-').map(int.parse).toList(); + var bNums = b.updateTime!.split('-').map(int.parse).toList(); + for (int i = 0; i < aNums.length; i++) { + if (aNums[i] != bNums[i]) { + return bNums[i] - aNums[i]; + } + } + return 0; + } catch (_) { + return 0; + } }); } @@ -270,6 +282,27 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { "Updates".tl, style: ts.s18, ), + const Spacer(), + if (updatedComics.isNotEmpty) + IconButton( + icon: Icon(Icons.clear_all), + onPressed: () { + showConfirmDialog( + context: App.rootContext, + title: "Mark all as read".tl, + content: "Do you want to mark all as read?".tl, + onConfirm: () { + for (var comic in updatedComics) { + LocalFavoritesManager().markAsRead( + comic.id, + comic.type, + ); + } + updateFollowUpdatesUI(); + }, + ); + }, + ), ], ), ), @@ -408,7 +441,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { } void setFolder(String folder) async { - FollowUpdatesService.cancelChecking?.call(); + FollowUpdatesService._cancelChecking?.call(); LocalFavoritesManager().prepareTableForFollowUpdates(folder); var count = LocalFavoritesManager().count(folder); @@ -447,7 +480,7 @@ class _FollowUpdatesPageState extends AutomaticGlobalState { } void checkNow() async { - FollowUpdatesService.cancelChecking?.call(); + FollowUpdatesService._cancelChecking?.call(); bool isCanceled = false; void onCancel() { @@ -570,7 +603,7 @@ void _updateFolderBase( tags: newTags, ); - LocalFavoritesManager().updateInfo(folder, item); + LocalFavoritesManager().updateInfo(folder, item, false); var updateTime = newInfo.findUpdateTime(); if (updateTime != null && updateTime != c.updateTime) { @@ -580,6 +613,8 @@ void _updateFolderBase( c.type, updateTime, ); + } else { + LocalFavoritesManager().updateCheckTime(folder, c.id, c.type); } updated++; return; @@ -606,6 +641,10 @@ void _updateFolderBase( await Future.wait(futures); + if (updated > 0) { + LocalFavoritesManager().notifyChanges(); + } + stream.close(); } @@ -617,12 +656,14 @@ Stream<_UpdateProgress> _updateFolder(String folder, bool ignoreCheckTime) { /// Background service for checking updates abstract class FollowUpdatesService { - static bool isChecking = false; + static bool _isChecking = false; - static void Function()? cancelChecking; + static void Function()? _cancelChecking; - static void check() async { - if (isChecking) { + static bool _isInitialized = false; + + static void _check() async { + if (_isChecking) { return; } var folder = appdata.settings["followUpdatesFolder"]; @@ -630,11 +671,16 @@ abstract class FollowUpdatesService { return; } bool isCanceled = false; - cancelChecking = () { + _cancelChecking = () { isCanceled = true; }; - isChecking = true; + _isChecking = true; + + while (DataSync().isDownloading) { + await Future.delayed(const Duration(milliseconds: 100)); + } + int updated = 0; try { await for (var progress in _updateFolder(folder, false)) { @@ -644,21 +690,27 @@ abstract class FollowUpdatesService { updated = progress.updated; } } finally { - cancelChecking = null; - isChecking = false; + _cancelChecking = null; + _isChecking = false; if (updated > 0) { updateFollowUpdatesUI(); } } } + /// Initialize the checker. static void initChecker() { - Timer.periodic(const Duration(hours: 1), (timer) { - check(); + if (_isInitialized) return; + _isInitialized = true; + _check(); + // A short interval will not affect the performance since every comic has a check time. + Timer.periodic(const Duration(minutes: 5), (timer) { + _check(); }); } } +/// Update the UI of follow updates. void updateFollowUpdatesUI() { GlobalState.findOrNull<_FollowUpdatesWidgetState>()?.updateCount(); GlobalState.findOrNull<_FollowUpdatesPageState>()?.updateComics(); diff --git a/lib/pages/history_page.dart b/lib/pages/history_page.dart index b4ea318..049021e 100644 --- a/lib/pages/history_page.dart +++ b/lib/pages/history_page.dart @@ -29,86 +29,211 @@ class _HistoryPageState extends State { void onUpdate() { setState(() { comics = HistoryManager().getAll(); + if (multiSelectMode) { + selectedComics.removeWhere((comic, _) => !comics.contains(comic)); + if (selectedComics.isEmpty) { + multiSelectMode = false; + } + } }); } var comics = HistoryManager().getAll(); - var controller = FlyoutController(); + bool multiSelectMode = false; + Map selectedComics = {}; + + void selectAll() { + setState(() { + selectedComics = comics.asMap().map((k, v) => MapEntry(v, true)); + }); + } + + void deSelect() { + setState(() { + selectedComics.clear(); + }); + } + + void invertSelection() { + setState(() { + comics.asMap().forEach((k, v) { + selectedComics[v] = !selectedComics.putIfAbsent(v, () => false); + }); + selectedComics.removeWhere((k, v) => !v); + }); + } + + void _removeHistory(History comic) { + if (comic.sourceKey.startsWith("Unknown")) { + HistoryManager().remove( + comic.id, + ComicType(int.parse(comic.sourceKey.split(':')[1])), + ); + } else if (comic.sourceKey == 'local') { + HistoryManager().remove( + comic.id, + ComicType.local, + ); + } else { + HistoryManager().remove( + comic.id, + ComicType(comic.sourceKey.hashCode), + ); + } + } + @override Widget build(BuildContext context) { - return Scaffold( - body: SmoothCustomScrollView( - slivers: [ - SliverAppbar( - title: Text('History'.tl), - actions: [ - Tooltip( - message: 'Clear History'.tl, - child: Flyout( - controller: controller, - flyoutBuilder: (context) { - return FlyoutContent( - title: 'Clear History'.tl, - content: Text( - 'Are you sure you want to clear your history?'.tl), - actions: [ - Button.filled( - color: context.colorScheme.error, - onPressed: () { - HistoryManager().clearHistory(); - context.pop(); - }, - child: Text('Clear'.tl), - ), - ], - ); - }, - child: IconButton( - icon: const Icon(Icons.clear_all), - onPressed: () { - controller.show(); - }, - ), - ), - ) - ], - ), - SliverGridComics( - comics: comics, - badgeBuilder: (c) { - return ComicSource.find(c.sourceKey)?.name; - }, - menuBuilder: (c) { - return [ - MenuEntry( - icon: Icons.remove, - text: 'Remove'.tl, + List selectActions = [ + IconButton( + icon: const Icon(Icons.select_all), + tooltip: "Select All".tl, + onPressed: selectAll + ), + IconButton( + icon: const Icon(Icons.deselect), + tooltip: "Deselect".tl, + onPressed: deSelect + ), + IconButton( + icon: const Icon(Icons.flip), + tooltip: "Invert Selection".tl, + onPressed: invertSelection + ), + IconButton( + icon: const Icon(Icons.delete), + tooltip: "Delete".tl, + onPressed: selectedComics.isEmpty + ? null + : () { + final comicsToDelete = List.from(selectedComics.keys); + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); + + for (final comic in comicsToDelete) { + _removeHistory(comic); + } + }, + ), + ]; + + List normalActions = [ + IconButton( + icon: const Icon(Icons.checklist), + tooltip: multiSelectMode ? "Exit Multi-Select".tl : "Multi-Select".tl, + onPressed: () { + setState(() { + multiSelectMode = !multiSelectMode; + }); + }, + ), + Tooltip( + message: 'Clear History'.tl, + child: Flyout( + controller: controller, + flyoutBuilder: (context) { + return FlyoutContent( + title: 'Clear History'.tl, + content: Text('Are you sure you want to clear your history?'.tl), + actions: [ + Button.filled( color: context.colorScheme.error, - onClick: () { - if (c.sourceKey.startsWith("Unknown")) { - HistoryManager().remove( - c.id, - ComicType(int.parse(c.sourceKey.split(':')[1])), - ); - } else if (c.sourceKey == 'local') { - HistoryManager().remove( - c.id, - ComicType.local, - ); + onPressed: () { + HistoryManager().clearHistory(); + context.pop(); + }, + child: Text('Clear'.tl), + ), + ], + ); + }, + child: IconButton( + icon: const Icon(Icons.clear_all), + onPressed: () { + controller.show(); + }, + ), + ), + ) + ]; + + return PopScope( + canPop: !multiSelectMode, + onPopInvokedWithResult: (didPop, result) { + if (multiSelectMode) { + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); + } + }, + child: Scaffold( + body: SmoothCustomScrollView( + slivers: [ + SliverAppbar( + leading: Tooltip( + message: multiSelectMode ? "Cancel".tl : "Back".tl, + child: IconButton( + onPressed: () { + if (multiSelectMode) { + setState(() { + multiSelectMode = false; + selectedComics.clear(); + }); } else { - HistoryManager().remove( - c.id, - ComicType(c.sourceKey.hashCode), - ); + context.pop(); } }, + icon: multiSelectMode + ? const Icon(Icons.close) + : const Icon(Icons.arrow_back), ), - ]; - }, - ), - ], + ), + title: multiSelectMode + ? Text(selectedComics.length.toString()) + : Text('History'.tl), + actions: multiSelectMode ? selectActions : normalActions, + ), + SliverGridComics( + comics: comics, + selections: selectedComics, + onLongPressed: null, + onTap: multiSelectMode + ? (c) { + setState(() { + if (selectedComics.containsKey(c as History)) { + selectedComics.remove(c); + } else { + selectedComics[c] = true; + } + if (selectedComics.isEmpty) { + multiSelectMode = false; + } + }); + } + : null, + badgeBuilder: (c) { + return ComicSource.find(c.sourceKey)?.name; + }, + menuBuilder: (c) { + return [ + MenuEntry( + icon: Icons.remove, + text: 'Remove'.tl, + color: context.colorScheme.error, + onClick: () { + _removeHistory(c as History); + }, + ), + ]; + }, + ), + ], + ), ), ); } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index f11e6d2..c715e22 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -197,10 +197,12 @@ class _HistoryState extends State<_History> { late int count; void onHistoryChange() { - setState(() { - history = HistoryManager().getRecent(); - count = HistoryManager().count(); - }); + if (mounted) { + setState(() { + history = HistoryManager().getRecent(); + count = HistoryManager().count(); + }); + } } @override @@ -603,6 +605,19 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { super.dispose(); } + int get _availableUpdates { + int c = 0; + ComicSource.availableUpdates.forEach((key, version) { + var source = ComicSource.find(key); + if (source != null) { + if (compareSemVer(version, source.version)) { + c++; + } + } + }); + return c; + } + @override Widget build(BuildContext context) { return SliverToBoxAdapter( @@ -666,7 +681,7 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { }).toList(), ).paddingHorizontal(16).paddingBottom(16), ), - if (ComicSource.availableUpdates.isNotEmpty) + if (_availableUpdates > 0) Container( padding: const EdgeInsets.symmetric( horizontal: 8, @@ -685,7 +700,7 @@ class _ComicSourceWidgetState extends State<_ComicSourceWidget> { Icon(Icons.update, color: context.colorScheme.primary, size: 20,), const SizedBox(width: 8), Text("@c updates".tlParams({ - 'c': ComicSource.availableUpdates.length, + 'c': _availableUpdates, }), style: ts.withColor(context.colorScheme.primary),), ], ), diff --git a/lib/pages/reader/chapters.dart b/lib/pages/reader/chapters.dart new file mode 100644 index 0000000..2b9a629 --- /dev/null +++ b/lib/pages/reader/chapters.dart @@ -0,0 +1,242 @@ +part of 'reader.dart'; + +class _ChaptersView extends StatefulWidget { + const _ChaptersView(this.reader); + + final _ReaderState reader; + + @override + State<_ChaptersView> createState() => _ChaptersViewState(); +} + +class _ChaptersViewState extends State<_ChaptersView> { + bool desc = false; + + late final ScrollController _scrollController; + + var downloaded = []; + + @override + void initState() { + super.initState(); + int epIndex = widget.reader.chapter - 2; + _scrollController = ScrollController( + initialScrollOffset: (epIndex * 48.0 + 52).clamp(0, double.infinity), + ); + var local = LocalManager().find(widget.reader.cid, widget.reader.type); + if (local != null) { + downloaded = local.downloadedChapters; + } + } + + @override + Widget build(BuildContext context) { + var chapters = widget.reader.widget.chapters!; + var current = widget.reader.chapter - 1; + return Scaffold( + body: SmoothCustomScrollView( + controller: _scrollController, + slivers: [ + SliverAppbar( + style: AppbarStyle.shadow, + title: Text("Chapters".tl), + actions: [ + Tooltip( + message: "Click to change the order".tl, + child: TextButton.icon( + icon: Icon( + !desc ? Icons.arrow_upward : Icons.arrow_downward, + size: 18, + ), + label: Text(!desc ? "Ascending".tl : "Descending".tl), + onPressed: () { + setState(() { + desc = !desc; + }); + }, + ), + ), + ], + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (desc) { + index = chapters.length - 1 - index; + } + var chapter = chapters.titles.elementAt(index); + return _ChapterListTile( + onTap: () { + widget.reader.toChapter(index + 1); + Navigator.of(context).pop(); + }, + title: chapter, + isActive: current == index, + isDownloaded: + downloaded.contains(chapters.ids.elementAt(index)), + ); + }, + childCount: chapters.length, + ), + ), + ], + ), + ); + } +} + +class _GroupedChaptersView extends StatefulWidget { + const _GroupedChaptersView(this.reader); + + final _ReaderState reader; + + @override + State<_GroupedChaptersView> createState() => _GroupedChaptersViewState(); +} + +class _GroupedChaptersViewState extends State<_GroupedChaptersView> + with SingleTickerProviderStateMixin { + ComicChapters get chapters => widget.reader.widget.chapters!; + + late final TabController tabController; + + late final ScrollController _scrollController; + + late final String initialGroupName; + + var downloaded = []; + + @override + void initState() { + super.initState(); + int index = 0; + int epIndex = widget.reader.chapter - 1; + while (epIndex >= 0) { + epIndex -= chapters.getGroupByIndex(index).length; + index++; + } + tabController = TabController( + length: chapters.groups.length, + vsync: this, + initialIndex: index - 1, + ); + initialGroupName = chapters.groups.elementAt(index - 1); + var epIndexAtGroup = widget.reader.chapter - 1; + for (var i = 0; i < index - 1; i++) { + epIndexAtGroup -= chapters.getGroupByIndex(i).length; + } + _scrollController = ScrollController( + initialScrollOffset: (epIndexAtGroup * 48.0).clamp(0, double.infinity), + ); + var local = LocalManager().find(widget.reader.cid, widget.reader.type); + if (local != null) { + downloaded = local.downloadedChapters; + } + } + + @override + Widget build(BuildContext context) { + return Column( + children: [ + Appbar(title: Text("Chapters".tl)), + AppTabBar( + controller: tabController, + tabs: chapters.groups.map((e) => Tab(text: e)).toList(), + ), + Expanded( + child: TabViewBody( + controller: tabController, + children: chapters.groups.map(buildGroup).toList(), + ), + ), + ], + ); + } + + Widget buildGroup(String groupName) { + var group = chapters.getGroup(groupName); + return SmoothCustomScrollView( + controller: initialGroupName == groupName ? _scrollController : null, + slivers: [ + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + var name = group.values.elementAt(index); + var i = 0; + for (var g in chapters.groups) { + if (g == groupName) { + break; + } + i += chapters.getGroup(g).length; + } + i += index + 1; + return _ChapterListTile( + onTap: () { + widget.reader.toChapter(i); + context.pop(); + }, + title: name, + isActive: widget.reader.chapter == i, + isDownloaded: downloaded.contains(group.keys.elementAt(index)), + ); + }, + childCount: group.length, + ), + ), + ], + ); + } +} + +class _ChapterListTile extends StatelessWidget { + const _ChapterListTile({ + required this.title, + required this.isActive, + required this.isDownloaded, + required this.onTap, + }); + + final String title; + + final bool isActive; + + final bool isDownloaded; + + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + child: Container( + height: 48, + padding: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: + isActive ? context.colorScheme.primary : Colors.transparent, + width: 4, + ), + ), + ), + child: Row( + children: [ + Text( + title, + style: isActive + ? ts.withColor(context.colorScheme.primary).bold.s16 + : ts.s16, + ), + const Spacer(), + if (isDownloaded) + Icon( + Icons.download_done_rounded, + color: context.colorScheme.secondary, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/reader/gesture.dart b/lib/pages/reader/gesture.dart index 3122339..0b3928c 100644 --- a/lib/pages/reader/gesture.dart +++ b/lib/pages/reader/gesture.dart @@ -131,11 +131,11 @@ class _ReaderGestureDetectorState extends AutomaticGlobalState<_ReaderGestureDet } if (context.reader.mode.key.startsWith('gallery')) { if (forward) { - if (!context.reader.toNextPage()) { + if (!context.reader.toNextPage() && !context.reader.isLastChapterOfGroup) { context.reader.toNextChapter(); } } else { - if (!context.reader.toPrevPage()) { + if (!context.reader.toPrevPage() && !context.reader.isFirstChapterOfGroup) { context.reader.toPrevChapter(); } } diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 7b8eee0..43932c4 100644 --- a/lib/pages/reader/images.dart +++ b/lib/pages/reader/images.dart @@ -45,7 +45,7 @@ class _ReaderImagesState extends State<_ReaderImages> { } else { var res = await reader.type.comicSource!.loadComicPages!( reader.widget.cid, - reader.widget.chapters?.keys.elementAt(reader.chapter - 1), + reader.widget.chapters?.ids.elementAt(reader.chapter - 1), ); if (res.error) { setState(() { @@ -154,7 +154,6 @@ class _GalleryModeState extends State<_GalleryMode> builder: (BuildContext context, int index) { if (index == 0 || index == totalPages + 1) { return PhotoViewGalleryPageOptions.customChild( - scaleStateController: PhotoViewScaleStateController(), child: const SizedBox(), ); } else { @@ -168,7 +167,7 @@ class _GalleryModeState extends State<_GalleryMode> cached[index] = true; cache(index); - photoViewControllers[index] = PhotoViewController(); + photoViewControllers[index] ??= PhotoViewController(); if (reader.imagesPerPage == 1) { return PhotoViewGalleryPageOptions( @@ -206,11 +205,11 @@ class _GalleryModeState extends State<_GalleryMode> ), onPageChanged: (i) { if (i == 0) { - if (!reader.toPrevChapter()) { + if (reader.isFirstChapterOfGroup || !reader.toPrevChapter()) { reader.toPage(1); } } else if (i == totalPages + 1) { - if (!reader.toNextChapter()) { + if (reader.isLastChapterOfGroup || !reader.toNextChapter()) { reader.toPage(totalPages); } } else { @@ -232,7 +231,7 @@ class _GalleryModeState extends State<_GalleryMode> ImageProvider imageProvider = _createImageProviderFromKey(imageKey, context); return Expanded( - child: Image( + child: ComicImage( image: imageProvider, fit: BoxFit.contain, ), @@ -350,6 +349,8 @@ const Set _kTouchLikeDeviceTypes = { PointerDeviceKind.unknown }; +const double _kChangeChapterOffset = 160; + class _ContinuousMode extends StatefulWidget { const _ContinuousMode({super.key}); @@ -364,7 +365,9 @@ class _ContinuousModeState extends State<_ContinuousMode> var itemScrollController = ItemScrollController(); var itemPositionsListener = ItemPositionsListener.create(); var photoViewController = PhotoViewController(); - late ScrollController scrollController; + ScrollController? _scrollController; + + ScrollController get scrollController => _scrollController!; var isCTRLPressed = false; static var _isMouseScrolling = false; @@ -372,6 +375,7 @@ class _ContinuousModeState extends State<_ContinuousMode> bool disableScroll = false; late List cached; + int get preCacheCount => appdata.settings["preloadImageCount"]; /// Whether the user was scrolling the page. @@ -386,6 +390,11 @@ class _ContinuousModeState extends State<_ContinuousMode> ); } + bool prepareToPrevChapter = false; + bool prepareToNextChapter = false; + bool jumpToNextChapter = false; + bool jumpToPrevChapter = false; + @override void initState() { reader = context.reader; @@ -406,6 +415,9 @@ class _ContinuousModeState extends State<_ContinuousMode> } void onPositionChanged() { + if (itemPositionsListener.itemPositions.value.isEmpty) { + return; + } var page = itemPositionsListener.itemPositions.value.first.index; page = page.clamp(1, reader.maxPage); if (page != reader.page) { @@ -461,6 +473,18 @@ class _ContinuousModeState extends State<_ContinuousMode> } } + void onScroll() { + if (prepareToPrevChapter) { + jumpToNextChapter = false; + jumpToPrevChapter = scrollController.offset < + scrollController.position.minScrollExtent - _kChangeChapterOffset; + } else if (prepareToNextChapter) { + jumpToNextChapter = scrollController.offset > + scrollController.position.maxScrollExtent + _kChangeChapterOffset; + jumpToPrevChapter = false; + } + } + @override Widget build(BuildContext context) { Widget widget = ScrollablePositionedList.builder( @@ -468,7 +492,11 @@ class _ContinuousModeState extends State<_ContinuousMode> itemScrollController: itemScrollController, itemPositionsListener: itemPositionsListener, scrollControllerCallback: (scrollController) { - this.scrollController = scrollController; + if (_scrollController != null) { + _scrollController!.removeListener(onScroll); + } + _scrollController = scrollController; + _scrollController!.addListener(onScroll); }, itemCount: reader.maxPage + 2, addSemanticIndexes: false, @@ -478,7 +506,7 @@ class _ContinuousModeState extends State<_ContinuousMode> reverse: reader.mode == ReaderMode.continuousRightToLeft, physics: isCTRLPressed || _isMouseScrolling || disableScroll ? const NeverScrollableScrollPhysics() - : const ClampingScrollPhysics(), + : const BouncingScrollPhysics(), itemBuilder: (context, index) { if (index == 0 || index == reader.maxPage + 1) { return const SizedBox(); @@ -493,18 +521,28 @@ class _ContinuousModeState extends State<_ContinuousMode> ImageProvider image = _createImageProvider(index, context); - return ComicImage( - filterQuality: FilterQuality.medium, - image: image, - width: width, - height: height, - fit: BoxFit.contain, + return ColoredBox( + color: context.colorScheme.surface, + child: ComicImage( + filterQuality: FilterQuality.medium, + image: image, + width: width, + height: height, + fit: BoxFit.contain, + ), ); }, scrollBehavior: const MaterialScrollBehavior() .copyWith(scrollbars: false, dragDevices: _kTouchLikeDeviceTypes), ); + widget = Stack( + children: [ + Positioned.fill(child: buildBackground(context)), + Positioned.fill(child: widget), + ], + ); + widget = Listener( onPointerDown: (event) { fingers++; @@ -527,6 +565,15 @@ class _ContinuousModeState extends State<_ContinuousMode> disableScroll = false; }); } + if (fingers == 0) { + if (jumpToPrevChapter) { + context.readerScaffold.setFloatingButton(0); + reader.toPrevChapter(); + } else if (jumpToNextChapter) { + context.readerScaffold.setFloatingButton(0); + reader.toNextChapter(); + } + } }, onPointerCancel: (event) { fingers--; @@ -572,18 +619,39 @@ class _ContinuousModeState extends State<_ContinuousMode> } if (notification is ScrollUpdateNotification) { - var length = reader.maxChapter; if (!scrollController.hasClients) return false; if (scrollController.position.pixels <= - scrollController.position.minScrollExtent && - reader.chapter != 1) { - context.readerScaffold.setFloatingButton(-1); + scrollController.position.minScrollExtent && + !reader.isFirstChapterOfGroup) { + if (!prepareToPrevChapter) { + jumpToPrevChapter = false; + jumpToNextChapter = false; + context.readerScaffold.setFloatingButton(-1); + setState(() { + prepareToPrevChapter = true; + }); + } } else if (scrollController.position.pixels >= - scrollController.position.maxScrollExtent && - reader.chapter < length) { - context.readerScaffold.setFloatingButton(1); + scrollController.position.maxScrollExtent && + !reader.isLastChapterOfGroup) { + if (!prepareToNextChapter) { + jumpToPrevChapter = false; + jumpToNextChapter = false; + context.readerScaffold.setFloatingButton(1); + setState(() { + prepareToNextChapter = true; + }); + } } else { context.readerScaffold.setFloatingButton(0); + if (prepareToPrevChapter || prepareToNextChapter) { + jumpToPrevChapter = false; + jumpToNextChapter = false; + setState(() { + prepareToPrevChapter = false; + prepareToNextChapter = false; + }); + } } } @@ -616,6 +684,26 @@ class _ContinuousModeState extends State<_ContinuousMode> ); } + Widget buildBackground(BuildContext context) { + return Column( + children: [ + SizedBox(height: context.padding.top + 16), + if (prepareToPrevChapter) + _SwipeChangeChapterProgress( + controller: scrollController, + isPrev: true, + ), + const Spacer(), + if (prepareToNextChapter) + _SwipeChangeChapterProgress( + controller: scrollController, + isPrev: false, + ), + SizedBox(height: 36), + ], + ); + } + @override Future animateToPage(int page) { return itemScrollController.scrollTo( @@ -756,3 +844,138 @@ void _precacheImage(int page, BuildContext context) { context, ); } + +class _SwipeChangeChapterProgress extends StatefulWidget { + const _SwipeChangeChapterProgress({ + this.controller, + required this.isPrev, + }); + + final ScrollController? controller; + + final bool isPrev; + + @override + State<_SwipeChangeChapterProgress> createState() => + _SwipeChangeChapterProgressState(); +} + +class _SwipeChangeChapterProgressState + extends State<_SwipeChangeChapterProgress> { + double value = 0; + + late final isPrev = widget.isPrev; + + ScrollController? controller; + + @override + void initState() { + super.initState(); + if (widget.controller != null) { + controller = widget.controller; + controller!.addListener(onScroll); + } + } + + @override + void didUpdateWidget(covariant _SwipeChangeChapterProgress oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.controller != widget.controller) { + controller?.removeListener(onScroll); + controller = widget.controller; + controller?.addListener(onScroll); + if (value != 0) { + setState(() { + value = 0; + }); + } + } + } + + @override + void dispose() { + super.dispose(); + controller?.removeListener(onScroll); + } + + void onScroll() { + var position = controller!.position.pixels; + var offset = isPrev + ? controller!.position.minScrollExtent - position + : position - controller!.position.maxScrollExtent; + var newValue = offset / _kChangeChapterOffset; + newValue = newValue.clamp(0.0, 1.0); + if (newValue != value) { + setState(() { + value = newValue; + }); + } + } + + @override + Widget build(BuildContext context) { + final msg = widget.isPrev + ? "Swipe down for previous chapter".tl + : "Swipe up for next chapter".tl; + + return CustomPaint( + painter: _ProgressPainter( + value: value, + backgroundColor: context.colorScheme.surfaceContainerLow, + color: context.colorScheme.surfaceContainerHighest, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + widget.isPrev ? Icons.arrow_downward : Icons.arrow_upward, + color: context.colorScheme.onSurface, + size: 16, + ), + const SizedBox(width: 4), + Text(msg), + ], + ).paddingVertical(6).paddingHorizontal(16), + ); + } +} + +class _ProgressPainter extends CustomPainter { + final double value; + + final Color backgroundColor; + + final Color color; + + const _ProgressPainter({ + required this.value, + required this.backgroundColor, + required this.color, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = backgroundColor + ..style = PaintingStyle.fill; + canvas.drawRRect( + RRect.fromLTRBR(0, 0, size.width, size.height, Radius.circular(16)), + paint, + ); + + paint.color = color; + canvas.drawRRect( + RRect.fromLTRBR( + 0, 0, size.width * value, size.height, Radius.circular(16)), + paint, + ); + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) { + return oldDelegate is! _ProgressPainter || + oldDelegate.value != value || + oldDelegate.backgroundColor != backgroundColor || + oldDelegate.color != color; + } +} diff --git a/lib/pages/reader/loading.dart b/lib/pages/reader/loading.dart index 38bfd57..d8fb732 100644 --- a/lib/pages/reader/loading.dart +++ b/lib/pages/reader/loading.dart @@ -33,6 +33,7 @@ class _ReaderWithLoadingState history: data.history, initialChapter: widget.initialEp ?? data.history.ep, initialPage: widget.initialPage ?? data.history.page, + initialChapterGroup: data.history.group, author: data.author, tags: data.tags, ); @@ -101,7 +102,7 @@ class ReaderProps { final String name; - final Map? chapters; + final ComicChapters? chapters; final History history; diff --git a/lib/pages/reader/reader.dart b/lib/pages/reader/reader.dart index ed1cb71..d1d0cea 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -40,11 +40,17 @@ import 'package:window_manager/window_manager.dart'; import 'package:battery_plus/battery_plus.dart'; part 'scaffold.dart'; + part 'images.dart'; + part 'gesture.dart'; + part 'comic_image.dart'; + part 'loading.dart'; +part 'chapters.dart'; + extension _ReaderContext on BuildContext { _ReaderState get reader => findAncestorStateOfType<_ReaderState>()!; @@ -62,6 +68,7 @@ class Reader extends StatefulWidget { required this.history, this.initialPage, this.initialChapter, + this.initialChapterGroup, required this.author, required this.tags, }); @@ -76,9 +83,7 @@ class Reader extends StatefulWidget { final String name; - /// key: Chapter ID, value: Chapter Name - /// null if the comic is a gallery - final Map? chapters; + final ComicChapters? chapters; /// Starts from 1, invalid values equal to 1 final int? initialPage; @@ -86,13 +91,17 @@ class Reader extends StatefulWidget { /// Starts from 1, invalid values equal to 1 final int? initialChapter; + /// Starts from 1, invalid values equal to 1 + final int? initialChapterGroup; + final History history; @override State createState() => _ReaderState(); } -class _ReaderState extends State with _ReaderLocation, _ReaderWindow { +class _ReaderState extends State + with _ReaderLocation, _ReaderWindow, _VolumeListener, _ImagePerPageHandler { @override void update() { setState(() {}); @@ -105,37 +114,15 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { String get cid => widget.cid; - String get eid => widget.chapters?.keys.elementAt(chapter - 1) ?? '0'; + String get eid => widget.chapters?.ids.elementAt(chapter - 1) ?? '0'; List? images; + @override late ReaderMode mode; - int get imagesPerPage => appdata.settings['readerScreenPicNumber'] ?? 1; - - int _lastImagesPerPage = appdata.settings['readerScreenPicNumber'] ?? 1; - @override - void didChangeDependencies() { - super.didChangeDependencies(); - _checkImagesPerPageChange(); - } - - void _checkImagesPerPageChange() { - int currentImagesPerPage = imagesPerPage; - if (_lastImagesPerPage != currentImagesPerPage) { - _adjustPageForImagesPerPageChange( - _lastImagesPerPage, currentImagesPerPage); - _lastImagesPerPage = currentImagesPerPage; - } - } - - void _adjustPageForImagesPerPageChange( - int oldImagesPerPage, int newImagesPerPage) { - int previousImageIndex = (page - 1) * oldImagesPerPage; - int newPage = (previousImageIndex ~/ newImagesPerPage) + 1; - page = newPage; - } + bool get isPortrait => MediaQuery.of(context).orientation == Orientation.portrait; History? history; @@ -144,18 +131,24 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { var focusNode = FocusNode(); - VolumeListener? volumeListener; - @override void initState() { page = widget.initialPage ?? 1; - chapter = widget.initialChapter ?? 1; if (page < 1) { page = 1; } + chapter = widget.initialChapter ?? 1; if (chapter < 1) { chapter = 1; } + if (widget.initialChapterGroup != null) { + for (int i = 0; i < (widget.initialChapterGroup! - 1); i++) { + chapter += widget.chapters!.getGroupByIndex(i).length; + } + } + if (widget.initialPage != null) { + page = widget.initialPage!; + } mode = ReaderMode.fromKey(appdata.settings['readerMode']); history = widget.history; Future.microtask(() { @@ -172,6 +165,12 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { super.initState(); } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + initImagesPerPage(widget.initialPage ?? 1); + } + void setImageCacheSize() async { var availableRAM = await MemoryInfo.getFreePhysicalMemorySize(); if (availableRAM == null) return; @@ -236,12 +235,28 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { void updateHistory() { if (history != null) { - history!.page = page; - history!.ep = chapter; - if (maxPage > 1) { - history!.maxPage = maxPage; + if (page == maxPage) { + /// Record the last image of chapter + history!.page = images?.length ?? 1; + } else { + /// Record the first image of the page + history!.page = (page - 1) * imagesPerPage + 1; + } + history!.maxPage = images?.length ?? 1; + if (widget.chapters?.isGrouped ?? false) { + int g = 0; + int c = chapter; + while (c > widget.chapters!.getGroupByIndex(g).length) { + c -= widget.chapters!.getGroupByIndex(g).length; + g++; + } + history!.readEpisode.add('${g + 1}-$c'); + history!.ep = c; + history!.group = g + 1; + } else { + history!.readEpisode.add(chapter.toString()); + history!.ep = chapter; } - history!.readEpisode.add(chapter); history!.time = DateTime.now(); _updateHistoryTimer?.cancel(); _updateHistoryTimer = Timer(const Duration(seconds: 1), () { @@ -251,6 +266,95 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { } } + bool get isFirstChapterOfGroup { + if (widget.chapters?.isGrouped ?? false) { + int c = chapter - 1; + int g = 1; + while (c > 0) { + c -= widget.chapters!.getGroupByIndex(g - 1).length; + g++; + } + if (c == 0) { + return true; + } else { + return false; + } + } + return chapter == 1; + } + + bool get isLastChapterOfGroup { + if (widget.chapters?.isGrouped ?? false) { + int c = chapter; + int g = 1; + while (c > 0) { + c -= widget.chapters!.getGroupByIndex(g - 1).length; + g++; + } + if (c == 0) { + return true; + } else { + return false; + } + } + return chapter == maxChapter; + } +} + +abstract mixin class _ImagePerPageHandler { + late int _lastImagesPerPage; + + bool get isPortrait; + + int get page; + + set page(int value); + + ReaderMode get mode; + + void initImagesPerPage(int initialPage) { + _lastImagesPerPage = imagesPerPage; + if (imagesPerPage != 1) { + page = (initialPage / imagesPerPage).ceil(); + } + } + + /// The number of images displayed on one screen + int get imagesPerPage { + if (mode.isContinuous) return 1; + if (isPortrait) { + return appdata.settings['readerScreenPicNumberForPortrait'] ?? 1; + } else { + return appdata.settings['readerScreenPicNumberForLandscape'] ?? 1; + } + } + + /// Check if the number of images per page has changed + void _checkImagesPerPageChange() { + int currentImagesPerPage = imagesPerPage; + if (_lastImagesPerPage != currentImagesPerPage) { + _adjustPageForImagesPerPageChange( + _lastImagesPerPage, currentImagesPerPage); + _lastImagesPerPage = currentImagesPerPage; + } + } + + /// Adjust the page number when the number of images per page changes + void _adjustPageForImagesPerPageChange( + int oldImagesPerPage, int newImagesPerPage) { + int previousImageIndex = (page - 1) * oldImagesPerPage; + int newPage = (previousImageIndex ~/ newImagesPerPage) + 1; + page = newPage; + } +} + +abstract mixin class _VolumeListener { + bool toNextPage(); + + bool toPrevPage(); + + VolumeListener? volumeListener; + void handleVolumeEvent() { if (!App.isAndroid) { // Currently only support Android @@ -260,12 +364,8 @@ class _ReaderState extends State with _ReaderLocation, _ReaderWindow { volumeListener?.cancel(); } volumeListener = VolumeListener( - onDown: () { - toNextPage(); - }, - onUp: () { - toPrevPage(); - }, + onDown: toNextPage, + onUp: toPrevPage, )..listen(); } diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 453081b..41ef5e6 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -26,73 +26,21 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { var lastValue = 0; - var fABValue = ValueNotifier(0); - _ReaderGestureDetectorState? _gestureDetectorState; - _DragListener? _floatingButtonDragListener; - void setFloatingButton(int value) { lastValue = showFloatingButtonValue; if (value == 0) { if (showFloatingButtonValue != 0) { showFloatingButtonValue = 0; - fABValue.value = 0; update(); } - if (_floatingButtonDragListener != null) { - _gestureDetectorState!.removeDragListener(_floatingButtonDragListener!); - _floatingButtonDragListener = null; - } } - var readerMode = context.reader.mode; if (value == 1 && showFloatingButtonValue == 0) { showFloatingButtonValue = 1; - _floatingButtonDragListener = _DragListener( - onMove: (offset) { - if (readerMode == ReaderMode.continuousTopToBottom) { - fABValue.value -= offset.dy; - } else if (readerMode == ReaderMode.continuousLeftToRight) { - fABValue.value -= offset.dx; - } else if (readerMode == ReaderMode.continuousRightToLeft) { - fABValue.value += offset.dx; - } - }, - onEnd: () { - if (fABValue.value.abs() > 58 * 3) { - setState(() { - showFloatingButtonValue = 0; - }); - context.reader.toNextChapter(); - } - fABValue.value = 0; - }, - ); - _gestureDetectorState!.addDragListener(_floatingButtonDragListener!); update(); } else if (value == -1 && showFloatingButtonValue == 0) { showFloatingButtonValue = -1; - _floatingButtonDragListener = _DragListener( - onMove: (offset) { - if (readerMode == ReaderMode.continuousTopToBottom) { - fABValue.value += offset.dy; - } else if (readerMode == ReaderMode.continuousLeftToRight) { - fABValue.value += offset.dx; - } else if (readerMode == ReaderMode.continuousRightToLeft) { - fABValue.value -= offset.dx; - } - }, - onEnd: () { - if (fABValue.value.abs() > 58 * 3) { - setState(() { - showFloatingButtonValue = 0; - }); - context.reader.toPrevChapter(); - } - fABValue.value = 0; - }, - ); - _gestureDetectorState!.addDragListener(_floatingButtonDragListener!); update(); } } @@ -279,7 +227,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { List tags = context.reader.widget.tags; String author = context.reader.widget.author; - var epName = context.reader.widget.chapters?.values + var epName = context.reader.widget.chapters?.titles .elementAtOrNull(context.reader.chapter - 1) ?? "E${context.reader.chapter}"; var translatedTags = tags.map((e) => e.translateTagsToCN).toList(); @@ -561,7 +509,7 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { } Widget buildPageInfoText() { - var epName = context.reader.widget.chapters?.values + var epName = context.reader.widget.chapters?.titles .elementAtOrNull(context.reader.chapter - 1) ?? "E${context.reader.chapter}"; if (epName.length > 8) { @@ -614,7 +562,9 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { void openChapterDrawer() { showSideBar( context, - _ChaptersView(context.reader), + context.reader.widget.chapters!.isGrouped + ? _GroupedChaptersView(context.reader) + : _ChaptersView(context.reader), width: 400, ); } @@ -776,62 +726,35 @@ class _ReaderScaffoldState extends State<_ReaderScaffold> { ); case -1: case 1: - return Container( + return SizedBox( width: 58, height: 58, - clipBehavior: Clip.antiAlias, - decoration: BoxDecoration( + child: Material( color: Theme.of(context).colorScheme.primaryContainer, borderRadius: BorderRadius.circular(16), - ), - child: ValueListenableBuilder( - valueListenable: fABValue, - builder: (context, value, child) { - return Stack( - children: [ - Positioned.fill( - child: Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - if (showFloatingButtonValue == 1) { - context.reader.toNextChapter(); - } else if (showFloatingButtonValue == -1) { - context.reader.toPrevChapter(); - } - setFloatingButton(0); - }, - borderRadius: BorderRadius.circular(16), - child: Center( - child: Icon( - showFloatingButtonValue == 1 - ? Icons.arrow_forward_ios - : Icons.arrow_back_ios_outlined, - size: 24, - color: Theme.of(context) - .colorScheme - .onPrimaryContainer, - ), - ), - ), - ), - ), - Positioned( - bottom: 0, - left: 0, - right: 0, - height: value.clamp(0, 58 * 3) / 3, - child: ColoredBox( - color: Theme.of(context) - .colorScheme - .surfaceTint - .toOpacity(0.2), - child: const SizedBox.expand(), - ), - ), - ], - ); - }, + elevation: 2, + child: InkWell( + onTap: () { + if (showFloatingButtonValue == 1) { + context.reader.toNextChapter(); + } else if (showFloatingButtonValue == -1) { + context.reader.toPrevChapter(); + } + setFloatingButton(0); + }, + borderRadius: BorderRadius.circular(16), + child: Center( + child: Icon( + showFloatingButtonValue == 1 + ? Icons.arrow_forward_ios + : Icons.arrow_back_ios_outlined, + size: 24, + color: Theme.of(context) + .colorScheme + .onPrimaryContainer, + ), + ), + ), ), ); } @@ -1017,79 +940,3 @@ class _ClockWidgetState extends State<_ClockWidget> { ); } } - -class _ChaptersView extends StatefulWidget { - const _ChaptersView(this.reader); - - final _ReaderState reader; - - @override - State<_ChaptersView> createState() => _ChaptersViewState(); -} - -class _ChaptersViewState extends State<_ChaptersView> { - bool desc = false; - - @override - Widget build(BuildContext context) { - var chapters = widget.reader.widget.chapters!; - var current = widget.reader.chapter - 1; - return Scaffold( - body: SmoothCustomScrollView( - slivers: [ - SliverAppbar( - title: Text("Chapters".tl), - actions: [ - Tooltip( - message: "Click to change the order".tl, - child: TextButton.icon( - icon: Icon( - !desc ? Icons.arrow_upward : Icons.arrow_downward, - size: 18, - ), - label: Text(!desc ? "Ascending".tl : "Descending".tl), - onPressed: () { - setState(() { - desc = !desc; - }); - }, - ), - ), - ], - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (desc) { - index = chapters.length - 1 - index; - } - var chapter = chapters.values.elementAt(index); - return ListTile( - shape: Border( - left: BorderSide( - color: current == index - ? context.colorScheme.primary - : Colors.transparent, - width: 4, - ), - ), - title: Text( - chapter, - style: current == index - ? ts.withColor(context.colorScheme.primary).bold - : null, - ), - onTap: () { - widget.reader.toChapter(index + 1); - Navigator.of(context).pop(); - }, - ); - }, - childCount: chapters.length, - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/settings/app.dart b/lib/pages/settings/app.dart index c324af9..4bb032f 100644 --- a/lib/pages/settings/app.dart +++ b/lib/pages/settings/app.dart @@ -330,10 +330,11 @@ class _WebdavSettingState extends State<_WebdavSetting> { String url = ""; String user = ""; String pass = ""; + bool autoSync = false; bool isTesting = false; - bool upload = true; + bool isEnabled = false; @override void initState() { @@ -348,6 +349,16 @@ class _WebdavSettingState extends State<_WebdavSetting> { url = configs[0]; user = configs[1]; pass = configs[2]; + isEnabled = true; + autoSync = appdata.implicitData['webdavAutoSync'] ?? false; + } + + void onAutoSyncChanged(bool value) { + setState(() { + autoSync = value; + appdata.implicitData['webdavAutoSync'] = value; + appdata.writeImplicitData(); + }); } @override @@ -357,6 +368,12 @@ class _WebdavSettingState extends State<_WebdavSetting> { body: SingleChildScrollView( child: Column( children: [ + const SizedBox(height: 12), + SwitchListTile( + title: Text("WebDAV Auto Sync".tl), + value: autoSync, + onChanged: onAutoSyncChanged, + ), const SizedBox(height: 12), TextField( decoration: const InputDecoration( @@ -411,12 +428,53 @@ class _WebdavSettingState extends State<_WebdavSetting> { ], ), const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.info_outline, size: 20), + const SizedBox(width: 8), + Expanded( + child: Text("Once the operation is successful, app will automatically sync data with the server.".tl), + ), + ], + ), + ), + const SizedBox(height: 16), Center( child: Button.filled( isLoading: isTesting, onPressed: () async { var oldConfig = appdata.settings['webdav']; + var oldAutoSync = appdata.implicitData['webdavAutoSync']; + + if (url.trim().isEmpty && + user.trim().isEmpty && + pass.trim().isEmpty) { + appdata.settings['webdav'] = []; + appdata.implicitData['webdavAutoSync'] = false; + appdata.writeImplicitData(); + appdata.saveData(); + context.showMessage(message: "Saved".tl); + App.rootPop(); + return; + } + appdata.settings['webdav'] = [url, user, pass]; + appdata.implicitData['webdavAutoSync'] = autoSync; + appdata.writeImplicitData(); + + if (!autoSync) { + appdata.saveData(); + context.showMessage(message: "Saved".tl); + App.rootPop(); + return; + } + setState(() { isTesting = true; }); @@ -428,12 +486,16 @@ class _WebdavSettingState extends State<_WebdavSetting> { isTesting = false; }); appdata.settings['webdav'] = oldConfig; + appdata.implicitData['webdavAutoSync'] = oldAutoSync; + appdata.writeImplicitData(); + appdata.saveData(); context.showMessage(message: testResult.errorMessage!); - return; + context.showMessage(message: "Saved Failed".tl); + } else { + appdata.saveData(); + context.showMessage(message: "Saved".tl); + App.rootPop(); } - appdata.saveData(); - context.showMessage(message: "Saved".tl); - App.rootPop(); }, child: Text("Continue".tl), ), diff --git a/lib/pages/settings/reader.dart b/lib/pages/settings/reader.dart index 7efb632..97e19fd 100644 --- a/lib/pages/settings/reader.dart +++ b/lib/pages/settings/reader.dart @@ -50,8 +50,10 @@ class _ReaderSettingsState extends State { onChanged: () { var readerMode = appdata.settings['readerMode']; if (readerMode?.toLowerCase().startsWith('continuous') ?? false) { - appdata.settings['readerScreenPicNumber'] = 1; - widget.onChanged?.call('readerScreenPicNumber'); + appdata.settings['readerScreenPicNumberForLandscape'] = 1; + widget.onChanged?.call('readerScreenPicNumberForLandscape'); + appdata.settings['readerScreenPicNumberForPortrait'] = 1; + widget.onChanged?.call('readerScreenPicNumberForPortrait'); } widget.onChanged?.call("readerMode"); }, @@ -81,13 +83,40 @@ class _ReaderSettingsState extends State { : 1.0, duration: Duration(milliseconds: 300), child: _SliderSetting( - title: "The number of pic in screen (Only Gallery Mode)".tl, - settingsIndex: "readerScreenPicNumber", + title: "The number of pic in screen for landscape (Only Gallery Mode)".tl, + settingsIndex: "readerScreenPicNumberForLandscape", interval: 1, min: 1, max: 5, onChanged: () { - widget.onChanged?.call("readerScreenPicNumber"); + widget.onChanged?.call("readerScreenPicNumberForLandscape"); + }, + ), + ), + ), + ), + SliverToBoxAdapter( + child: AbsorbPointer( + absorbing: (appdata.settings['readerMode'] + ?.toLowerCase() + .startsWith('continuous') ?? + false), + child: AnimatedOpacity( + opacity: (appdata.settings['readerMode'] + ?.toLowerCase() + .startsWith('continuous') ?? + false) + ? 0.5 + : 1.0, + duration: Duration(milliseconds: 300), + child: _SliderSetting( + title: "The number of pic in screen for portrait (Only Gallery Mode)".tl, + settingsIndex: "readerScreenPicNumberForPortrait", + interval: 1, + min: 1, + max: 5, + onChanged: () { + widget.onChanged?.call("readerScreenPicNumberForPortrait"); }, ), ), diff --git a/lib/utils/cbz.dart b/lib/utils/cbz.dart index 0bd00d2..dcd68d0 100644 --- a/lib/utils/cbz.dart +++ b/lib/utils/cbz.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:flutter_7zip/flutter_7zip.dart'; import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/utils/ext.dart'; @@ -176,7 +177,7 @@ abstract class CBZ { tags: metaData.tags, comicType: ComicType.local, directory: dest.name, - chapters: cpMap, + chapters: ComicChapters.fromJson(cpMap), downloadedChapters: cpMap?.keys.toList() ?? [], cover: 'cover.${coverFile.extension}', createdAt: DateTime.now(), diff --git a/lib/utils/data_sync.dart b/lib/utils/data_sync.dart index 4ee6435..e73cd9f 100644 --- a/lib/utils/data_sync.dart +++ b/lib/utils/data_sync.dart @@ -32,23 +32,31 @@ class DataSync with ChangeNotifier { factory DataSync() => instance ?? (instance = DataSync._()); - bool isDownloading = false; + bool _isDownloading = false; - bool isUploading = false; + bool get isDownloading => _isDownloading; + + bool _isUploading = false; + + bool get isUploading => _isUploading; bool haveWaitingTask = false; bool get isEnabled { var config = appdata.settings['webdav']; - return config is List && config.isNotEmpty; + var autoSync = appdata.implicitData['webdavAutoSync'] ?? false; + return autoSync && config is List && config.isNotEmpty; } List? _validateConfig() { var config = appdata.settings['webdav']; - if (config is! List || (config.isNotEmpty && config.length != 3)) { + if (config is! List) { return null; } - if (config.whereType().length != 3) { + if (config.isEmpty) { + return []; + } + if (config.length != 3 || config.whereType().length != 3) { return null; } return List.from(config); @@ -62,7 +70,7 @@ class DataSync with ChangeNotifier { await Future.delayed(const Duration(milliseconds: 100)); } haveWaitingTask = false; - isUploading = true; + _isUploading = true; notifyListeners(); try { var config = _validateConfig(); @@ -126,7 +134,7 @@ class DataSync with ChangeNotifier { return Res.error(e.toString()); } } finally { - isUploading = false; + _isUploading = false; notifyListeners(); } } @@ -138,7 +146,7 @@ class DataSync with ChangeNotifier { await Future.delayed(const Duration(milliseconds: 100)); } haveWaitingTask = false; - isDownloading = true; + _isDownloading = true; notifyListeners(); try { var config = _validateConfig(); @@ -201,7 +209,7 @@ class DataSync with ChangeNotifier { return Res.error(e.toString()); } } finally { - isDownloading = false; + _isDownloading = false; notifyListeners(); } } diff --git a/lib/utils/import_comic.dart b/lib/utils/import_comic.dart index 7473c06..ecb186b 100644 --- a/lib/utils/import_comic.dart +++ b/lib/utils/import_comic.dart @@ -3,6 +3,7 @@ import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:venera/components/components.dart'; import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/comic_source/comic_source.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/favorites.dart'; import 'package:venera/foundation/local.dart'; @@ -262,7 +263,9 @@ class ImportComic { subtitle: subtitle ?? '', tags: tags ?? [], directory: directory.path, - chapters: hasChapters ? Map.fromIterables(chapters, chapters) : null, + chapters: hasChapters + ? ComicChapters(Map.fromIterables(chapters, chapters)) + : null, cover: coverPath, comicType: ComicType.local, downloadedChapters: chapters, diff --git a/pubspec.lock b/pubspec.lock index d35693e..3a12883 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: "direct main" description: name: app_links - sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950" + sha256: "85ed8fc1d25a76475914fff28cc994653bd900bc2c26e4b57a49e097febb54ba" url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.4.0" app_links_linux: dependency: transitive description: @@ -141,10 +141,10 @@ packages: dependency: transitive description: name: dbus - sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" desktop_webview_window: dependency: "direct main" description: @@ -158,18 +158,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" + sha256: "253a18bbd4851fecba42f7343a1df3a9a4c1d31a2c1b37e221086b4fa8c8dbc9" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.8.0+1" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + sha256: e485c7a39ff2b384fa1d7e09b4e25f755804de8384358049124830b04fc4f93a url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" dynamic_color: dependency: "direct main" description: @@ -190,10 +190,10 @@ packages: dependency: transitive description: name: ffi - sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" file: dependency: transitive description: @@ -262,10 +262,10 @@ packages: dependency: transitive description: name: file_selector_windows - sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + sha256: "320fcfb6f33caa90f0b58380489fc5ac05d99ee94b61aa96ec2bff0ba81d3c2b" url: "https://pub.dev" source: hosted - version: "0.9.3+3" + version: "0.9.3+4" fixnum: dependency: transitive description: @@ -400,10 +400,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" url: "https://pub.dev" source: hosted - version: "2.0.23" + version: "2.0.24" flutter_qjs: dependency: "direct main" description: @@ -417,18 +417,18 @@ packages: dependency: "direct main" description: name: flutter_reorderable_grid_view - sha256: "732bcb1b29d5130c11a70e6acec512941fafe241f0e80bffd93ca6e415819915" + sha256: a7e0f9d5ba12fd232eb07fbb7f570ae35491045a6bba1858f6eb50c675526dfe url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.4.1" flutter_rust_bridge: dependency: transitive description: name: flutter_rust_bridge - sha256: "35c257fc7f98e34c1314d6c145e5ed54e7c94e8a9f469947e31c9298177d546f" + sha256: "3292ad6085552987b8b3b9a7e5805567f4013372d302736b702801acb001ee00" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" flutter_saf: dependency: "direct main" description: @@ -492,18 +492,18 @@ packages: dependency: transitive description: name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" http_parser: dependency: transitive description: name: http_parser - sha256: "76d306a1c3afb33fe82e2bbacad62a61f409b5634c915fceb0d799de1a913360" + sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571" url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "4.1.2" http_profile: dependency: transitive description: @@ -528,14 +528,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: c1b2e9b5ea78c45e1a0788d29606ba27dc5f71f019f32ca5140f61ef071838cf - url: "https://pub.dev" - source: hosted - version: "0.7.1" json_annotation: dependency: transitive description: @@ -572,10 +564,10 @@ packages: dependency: transitive description: name: lints - sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.1.1" local_auth: dependency: "direct main" description: @@ -596,10 +588,10 @@ packages: dependency: transitive description: name: local_auth_darwin - sha256: "6d2950da311d26d492a89aeb247c72b4653ddc93601ea36a84924a396806d49c" + sha256: "630996cd7b7f28f5ab92432c4b35d055dd03a747bc319e5ffbb3c4806a3e50d2" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.3" local_auth_platform_interface: dependency: transitive description: @@ -725,16 +717,16 @@ packages: dependency: transitive description: name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + sha256: "07c8f0b1913bcde1ff0d26e57ace2f3012ccbf2b204e070290dad3bb22797646" url: "https://pub.dev" source: hosted - version: "6.0.2" + version: "6.1.0" photo_view: dependency: "direct main" description: path: "." - ref: "94724a0b" - resolved-ref: "94724a0b7f94167fd1ae061f84e14ae04cae5c39" + ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 + resolved-ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 url: "https://github.com/wgh136/photo_view" source: git version: "0.14.0" @@ -758,18 +750,18 @@ packages: dependency: "direct main" description: name: pointycastle - sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + sha256: "92aa3841d083cc4b0f4709b5c74fd6409a3e6ba833ffc7dc6a8fee096366acf5" url: "https://pub.dev" source: hosted - version: "3.9.1" + version: "4.0.0" rhttp: dependency: "direct main" description: name: rhttp - sha256: "8212cbc816cc3e761eecb8d4dbbaa1eca95de715428320a198a4e7a89acdcd2e" + sha256: "3deabc6c3384b4efa252dfb4a5059acc6530117fdc1b10f5f67ff9768c9af75a" url: "https://pub.dev" source: hosted - version: "0.9.8" + version: "0.10.0" screen_retriever: dependency: transitive description: @@ -823,10 +815,10 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "6327c3f233729374d0abaafd61f6846115b2a481b4feddd8534211dc10659400" + sha256: fce43200aa03ea87b91ce4c3ac79f0cecd52e2a7a56c7a4185023c271fbfa6da url: "https://pub.dev" source: hosted - version: "10.1.3" + version: "10.1.4" share_plus_platform_interface: dependency: transitive description: @@ -876,18 +868,18 @@ packages: dependency: "direct main" description: name: sqlite3 - sha256: cb7f4e9dc1b52b1fa350f7b3d41c662e75fc3d399555fa4e5efcf267e9a4fbb5 + sha256: "32b632dda27d664f85520093ed6f735ae5c49b5b75345afb8b19411bc59bb53d" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.7.4" sqlite3_flutter_libs: dependency: "direct main" description: name: sqlite3_flutter_libs - sha256: "73016db8419f019e807b7a5e5fbf2a7bd45c165fed403b8e7681230f3a102785" + sha256: "57fafacd815c981735406215966ff7caaa8eab984b094f52e692accefcbd9233" url: "https://pub.dev" source: hosted - version: "0.5.28" + version: "0.5.30" stack_trace: dependency: transitive description: @@ -1004,18 +996,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" + sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.0" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" + sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "3.1.4" uuid: dependency: "direct main" description: @@ -1061,10 +1053,10 @@ packages: dependency: transitive description: name: win32 - sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" + sha256: b89e6e24d1454e149ab20fbb225af58660f0c0bf4475544650700d8e2da54aef url: "https://pub.dev" source: hosted - version: "5.9.0" + version: "5.11.0" window_manager: dependency: "direct main" description: @@ -1106,5 +1098,5 @@ packages: source: hosted version: "0.0.10" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.7.0 <4.0.0" flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 97295c1..f5e3026 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: venera description: "A comic app." publish_to: 'none' -version: 1.3.0+130 +version: 1.3.1+131 environment: sdk: '>=3.6.0 <4.0.0' @@ -14,24 +14,24 @@ dependencies: path_provider: any intl: any window_manager: ^0.4.3 - sqlite3: ^2.4.7 - sqlite3_flutter_libs: ^0.5.28 + sqlite3: ^2.7.4 + sqlite3_flutter_libs: ^0.5.30 flutter_qjs: git: url: https://github.com/wgh136/flutter_qjs ref: 5978d0c7784fbbefcacc573547f0ab01ba59b7b3 crypto: ^3.0.6 - dio: ^5.7.0 + dio: ^5.8.0+1 html: ^0.15.5 - pointycastle: ^3.9.1 + pointycastle: ^4.0.0 url_launcher: ^6.3.0 path: ^1.9.0 photo_view: git: url: https://github.com/wgh136/photo_view - ref: 94724a0b + ref: d71faf3c75e059d013b0ce9ddaf5ecc1680e2eb6 mime: ^2.0.0 - share_plus: ^10.1.3 + share_plus: ^10.1.4 scrollable_positioned_list: git: url: https://github.com/venera-app/flutter.widgets @@ -48,7 +48,7 @@ dependencies: url: https://github.com/pichillilorenzo/flutter_inappwebview path: flutter_inappwebview ref: 0aaf7a0bfc01d61a4d1453cefb57fb6783b6e676 - app_links: ^6.3.3 + app_links: ^6.4.0 sliver_tools: ^0.2.12 flutter_file_dialog: ^3.0.2 file_selector: ^1.0.3 @@ -57,7 +57,7 @@ dependencies: git: url: https://github.com/venera-app/lodepng_flutter ref: 9a784b193af5d55b2a35e58fa390bda3e4f35d00 - rhttp: 0.9.8 + rhttp: 0.10.0 webdav_client: git: url: https://github.com/wgh136/webdav_client