diff --git a/lib/pages/comic_page.dart b/lib/pages/comic_page.dart index 114db277c..e5f63a004 100644 --- a/lib/pages/comic_page.dart +++ b/lib/pages/comic_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:pica_comic/base.dart'; import 'package:pica_comic/comic_source/comic_source.dart'; +import 'package:pica_comic/network/custom_download_model.dart'; import 'package:pica_comic/components/comment.dart'; import 'package:pica_comic/components/components.dart'; import 'package:pica_comic/components/select_download_eps.dart'; @@ -18,6 +19,7 @@ import 'package:pica_comic/foundation/log.dart'; import 'package:pica_comic/foundation/stack.dart' as stack; import 'package:pica_comic/foundation/ui_mode.dart'; import 'package:pica_comic/network/base_comic.dart'; +import 'package:pica_comic/network/download_model.dart'; import 'package:pica_comic/network/download.dart'; import 'package:pica_comic/network/res.dart'; import 'package:pica_comic/pages/favorites/local_favorites.dart'; @@ -121,6 +123,64 @@ class _ComicPageImpl extends BaseComicPage { } } + @override + Future loadLocalData() async { + final downloadId = DownloadManager().generateId(sourceKey, id); + + // 优先级1:已下载的记录 + DownloadedItem? downloaded; + try { + if (DownloadManager().isExists(downloadId)) { + downloaded = await DownloadManager().getComicOrNull(downloadId); + } + } catch (_) {} + + if (downloaded != null) { + return ComicInfoData( + downloaded.name, + downloaded.subTitle, + _logic.history?.cover ?? '', + null, // description: 本地无 + _tagsListToMap(downloaded.tags), // tags: 从下载记录恢复 + downloaded is CustomDownloadedItem + ? (downloaded as CustomDownloadedItem).chapters + : null, // chapters: 仅 CustomDownloadedItem 有 + null, // thumbnails: 本地无 + null, // thumbnailLoader: 本地无 + 0, + null, // suggestions: 本地无 + sourceKey, + id, + ); + } + + // 优先级2:阅读历史(title、cover、subtitle 已在 get() 中查询,存在 _logic.history 中) + final h = _logic.history; + if (h != null && h.title.isNotEmpty) { + return ComicInfoData( + h.title, + h.subtitle, + h.cover, + null, // description + {}, // tags: 无 + null, // chapters: 无 + null, // thumbnails + null, // thumbnailLoader + 0, + null, // suggestions + sourceKey, + id, + ); + } + return null; + } + + /// 辅助方法:将 List tags 转为 Map> + static Map> _tagsListToMap(List tags) { + if (tags.isEmpty) return {}; + return {"tags": tags}; + } + @override EpsData? get eps { if (data!.chapters != null && data!.chapters!.isNotEmpty) { @@ -160,7 +220,12 @@ class _ComicPageImpl extends BaseComicPage { @override Future loadFavorite(ComicInfoData data) async { - return data.isFavorite ?? false; + // 平台收藏状态(网络返回),无平台收藏功能的源此值为 null + bool platformFavorite = data.isFavorite ?? false; + // 本地收藏状态(SQLite),isExist() 只按 target 匹配,不受 FavoriteType 不一致影响 + bool localFavorite = LocalFavoritesManager().isExist(id); + // 网络或本地,任意一边为 true → 显示"已收藏" + return platformFavorite || localFavorite; } @override @@ -598,29 +663,59 @@ class ComicPageLogic extends StateController { bool showFullEps = false; int colorIndex = 0; bool? favoriteOnPlatform; + String? networkMessage;/// 网络加载失败时的信息(有本地数据时网络失败用 toast 展示,不阻塞页面) - void get(Future> Function() loadData, - Future Function(T) loadFavorite, String Function() getId) async { - var [res, _] = await Future.wait( - [loadData(), Future.delayed(const Duration(milliseconds: 300))]); - if (res.error) { + + Future Function()? loadLocalData;/// 由 [BaseComicPage] 在首次构建时注入,指向子类的 loadLocalData 方法 + +void get(Future> Function() loadData, + Future Function(T) loadFavorite, String Function() getId) async { + // ======== 阶段1:加载本地数据和历史 ======== + history = await HistoryManager().find(getId()); + + if (loadLocalData != null) { + var local = await loadLocalData!(); + if (local != null) { + // 本地有数据,立即展示页面 + data = local; + favorite = await loadFavorite(local); + loading = false; + update(); // ← 页面立即渲染,看到标题、封面、操作按钮 + } + } + + // ======== 阶段2:加载网络数据 ======== + var [res, _] = await Future.wait( + [loadData(), Future.delayed(const Duration(milliseconds: 300))]); + + if (res.error) { + if (data == null) { + // 无本地数据 且 网络失败 → 显示错误(现有行为保持) message = res.errorMessage; if (message == "Exit") { loading = false; return; } } else { - data = res.data; - favorite = await loadFavorite(res.data); + // 有本地数据 但 网络失败 → 保留本地数据,仅记录错误 + networkMessage = res.errorMessage; } - loading = false; - history = await HistoryManager().find(getId()); - update(); + } else { + // 网络成功 → 用完整数据替换本地数据 + data = res.data; + favorite = await loadFavorite(res.data); + message = null; + networkMessage = null; } + loading = false; + update(); +} + void refresh_() { data = null; message = null; + networkMessage = null; loading = true; update(); } @@ -727,6 +822,8 @@ abstract class BaseComicPage extends StatelessWidget { @nonVirtual set favorite(bool f) => _logic.favorite = f; + History? get pageHistory => _logic.history; + Future loadFavorite(T data); /// used for history @@ -791,6 +888,7 @@ abstract class BaseComicPage extends StatelessWidget { _logic.width = constraints.maxWidth; _logic.height = constraints.maxHeight; if (logic.loading) { + logic.loadLocalData = loadLocalData; logic.get(loadData, loadFavorite, () => id); return buildLoading(context); } else if (logic.message != null) { @@ -802,6 +900,12 @@ abstract class BaseComicPage extends StatelessWidget { _logic.thumbnailsData ??= thumbnailsCreator; logic.controller.removeListener(scrollListener); logic.controller.addListener(scrollListener); + if (logic.networkMessage != null) { + WidgetsBinding.instance.addPostFrameCallback((_) { + showToast(message: logic.networkMessage!); + logic.networkMessage = null; + }); + } return SmoothCustomScrollView( controller: logic.controller, slivers: [ @@ -1681,6 +1785,10 @@ abstract class BaseComicPage extends StatelessWidget { ); } } + /// 从本地数据源加载漫画信息。 + /// 返回非 null 时,页面将立即渲染本地数据,同时后台继续加载网络数据。 + /// 默认返回 null(保持现有行为,不启用本地加载)。 + Future loadLocalData() async => null; } class FavoriteComicWidget extends StatefulWidget { diff --git a/lib/pages/ehentai/eh_gallery_page.dart b/lib/pages/ehentai/eh_gallery_page.dart index e578df23a..e110c6958 100644 --- a/lib/pages/ehentai/eh_gallery_page.dart +++ b/lib/pages/ehentai/eh_gallery_page.dart @@ -51,7 +51,51 @@ class EhGalleryPage extends BaseComicPage { ), ); }; + + @override + Future loadLocalData() async { + String title = '', cover = '', subTitle = ''; + bool found = false; + final h = pageHistory; + if (h != null && h.title.isNotEmpty) { + title = h.title; cover = h.cover; subTitle = h.subtitle; found = true; + } + if (!found) { + try { + final dlId = DownloadManager().generateId(sourceKey, id); + if (DownloadManager().isExists(dlId)) { + final dl = await DownloadManager().getComicOrNull(dlId); + if (dl != null && dl.name.isNotEmpty) { + title = dl.name; subTitle = dl.subTitle; + cover = 'file://${DownloadManager().path}/${DownloadManager().getDirectory(dlId)}/cover.jpg'; + found = true; + } + } + } catch (_) {} + } + if (!found) { + try { + final folders = await LocalFavoritesManager().find(id, const FavoriteType(1)); + if (folders.isNotEmpty) { + final item = LocalFavoritesManager().getComic(folders.first, id, const FavoriteType(1)); + title = item.name; subTitle = item.author; cover = item.coverPath; found = true; + } + } catch (_) {} + } + if (!found) return null; + try { + return Gallery.fromJson({ + "title": title, "subTitle": subTitle, + "type": "", "time": "", "uploader": subTitle, + "stars": 0.0, "rating": null, "coverPath": cover, + "tags": >{}, "favorite": false, + "link": id, "maxPage": "0", "pageSize": 20, + "ext": "jpg", "width": 100 + }); + } catch (_) { return null; } + } + @override String? get cover => (comicCover ?? data?.coverPath) ?.replaceFirst("s.exhentai.org", "ehgt.org"); diff --git a/lib/pages/hitomi/hitomi_comic_page.dart b/lib/pages/hitomi/hitomi_comic_page.dart index 0a0084bf3..f6800998b 100644 --- a/lib/pages/hitomi/hitomi_comic_page.dart +++ b/lib/pages/hitomi/hitomi_comic_page.dart @@ -210,6 +210,46 @@ class HitomiComicPage extends BaseComicPage { @override String get sourceKey => "hitomi"; + + @override + Future loadLocalData() async { + String title = '', cover = '', subTitle = ''; + bool found = false; + final h = pageHistory; + if (h != null && h.title.isNotEmpty) { + title = h.title; cover = h.cover; subTitle = h.subtitle; found = true; + } + if (!found) { + try { + final dlId = DownloadManager().generateId(sourceKey, id); + if (DownloadManager().isExists(dlId)) { + final dl = await DownloadManager().getComicOrNull(dlId); + if (dl != null && dl.name.isNotEmpty) { + title = dl.name; subTitle = dl.subTitle; + cover = 'file://${DownloadManager().path}/${DownloadManager().getDirectory(dlId)}/cover.jpg'; + found = true; + } + } + } catch (_) {} + } + if (!found) { + try { + final folders = await LocalFavoritesManager().find(id, const FavoriteType(3)); + if (folders.isNotEmpty) { + final item = LocalFavoritesManager().getComic(folders.first, id, const FavoriteType(3)); + title = item.name; subTitle = item.author; cover = item.coverPath; found = true; + } + } catch (_) {} + } + if (!found) return null; + try { + return HitomiComic( + link, title, [], "", [subTitle], + "", [], [], [], "", + [], [], cover, + ); + } catch (_) { return null; } + } } void _downloadComic( diff --git a/lib/pages/htmanga/ht_comic_page.dart b/lib/pages/htmanga/ht_comic_page.dart index afe3232d3..0e5684e22 100644 --- a/lib/pages/htmanga/ht_comic_page.dart +++ b/lib/pages/htmanga/ht_comic_page.dart @@ -177,6 +177,47 @@ class HtComicPage extends BaseComicPage { @override String get sourceKey => 'htmanga'; + + @override + Future loadLocalData() async { + String title = '', cover = '', subTitle = ''; + bool found = false; + final h = pageHistory; + if (h != null && h.title.isNotEmpty) { + title = h.title; cover = h.cover; subTitle = h.subtitle; found = true; + } + if (!found) { + try { + final dlId = DownloadManager().generateId(sourceKey, id); + if (DownloadManager().isExists(dlId)) { + final dl = await DownloadManager().getComicOrNull(dlId); + if (dl != null && dl.name.isNotEmpty) { + title = dl.name; subTitle = dl.subTitle; + cover = 'file://${DownloadManager().path}/${DownloadManager().getDirectory(dlId)}/cover.jpg'; + found = true; + } + } + } catch (_) {} + } + if (!found) { + try { + final folders = await LocalFavoritesManager().find(id, const FavoriteType(4)); + if (folders.isNotEmpty) { + final item = LocalFavoritesManager().getComic(folders.first, id, const FavoriteType(4)); + title = item.name; subTitle = item.author; cover = item.coverPath; found = true; + } + } catch (_) {} + } + if (!found) return null; + try { + return HtComicInfo.fromJson({ + "id": id, "coverPath": cover, "name": title, + "category": "", "pages": 0, "tags": {}, + "description": "", "uploader": subTitle, + "avatar": "", "uploadNum": 0 + }); + } catch (_) { return null; } + } } class HtComicPageLogic extends StateController { diff --git a/lib/pages/jm/jm_comic_page.dart b/lib/pages/jm/jm_comic_page.dart index e85b578d3..9256c8e28 100644 --- a/lib/pages/jm/jm_comic_page.dart +++ b/lib/pages/jm/jm_comic_page.dart @@ -199,6 +199,48 @@ class JmComicPage extends BaseComicPage { @override String get sourceKey => "jm"; + + + @override + Future loadLocalData() async { + String title = '', cover = '', subTitle = ''; + bool found = false; + final h = pageHistory; + if (h != null && h.title.isNotEmpty) { + title = h.title; cover = h.cover; subTitle = h.subtitle; found = true; + } + if (!found) { + try { + final dlId = DownloadManager().generateId(sourceKey, id); + if (DownloadManager().isExists(dlId)) { + final dl = await DownloadManager().getComicOrNull(dlId); + if (dl != null && dl.name.isNotEmpty) { + title = dl.name; subTitle = dl.subTitle; + cover = 'file://${DownloadManager().path}/${DownloadManager().getDirectory(dlId)}/cover.jpg'; + found = true; + } + } + } catch (_) {} + } + if (!found) { + try { + final folders = await LocalFavoritesManager().find(id, const FavoriteType(2)); + if (folders.isNotEmpty) { + final item = LocalFavoritesManager().getComic(folders.first, id, const FavoriteType(2)); + title = item.name; subTitle = item.author; cover = item.coverPath; found = true; + } + } catch (_) {} + } + if (!found) return null; + try { + return JmComicInfo.fromMap({ + "name": title, "id": id, "author": [subTitle], + "description": "", "series": {}, + "tags": [], "works": [], + "actors": [], "epNames": [] + }); + } catch (_) { return null; } + } } void downloadComic(JmComicInfo comic, BuildContext context) async { diff --git a/lib/pages/nhentai/comic_page.dart b/lib/pages/nhentai/comic_page.dart index d65c2161b..47dc18f4b 100644 --- a/lib/pages/nhentai/comic_page.dart +++ b/lib/pages/nhentai/comic_page.dart @@ -244,4 +244,43 @@ class NhentaiComicPage extends BaseComicPage { @override String get sourceKey => 'nhentai'; + + @override + Future loadLocalData() async { + String title = '', cover = '', subTitle = ''; + bool found = false; + final h = pageHistory; + if (h != null && h.title.isNotEmpty) { + title = h.title; cover = h.cover; subTitle = h.subtitle; found = true; + } + if (!found) { + try { + final dlId = DownloadManager().generateId(sourceKey, id); + if (DownloadManager().isExists(dlId)) { + final dl = await DownloadManager().getComicOrNull(dlId); + if (dl != null && dl.name.isNotEmpty) { + title = dl.name; subTitle = dl.subTitle; + cover = 'file://${DownloadManager().path}/${DownloadManager().getDirectory(dlId)}/cover.jpg'; + found = true; + } + } + } catch (_) {} + } + if (!found) { + try { + final folders = await LocalFavoritesManager().find(id, const FavoriteType(6)); + if (folders.isNotEmpty) { + final item = LocalFavoritesManager().getComic(folders.first, id, const FavoriteType(6)); + title = item.name; subTitle = item.author; cover = item.coverPath; found = true; + } + } catch (_) {} + } + if (!found) return null; + try { + return NhentaiComic.fromMap({ + "id": id, "title": title, + "subTitle": subTitle, "cover": cover, + }); + } catch (_) { return null; } + } } diff --git a/lib/pages/picacg/comic_page.dart b/lib/pages/picacg/comic_page.dart index 1ec324790..3a1a9a67a 100644 --- a/lib/pages/picacg/comic_page.dart +++ b/lib/pages/picacg/comic_page.dart @@ -40,7 +40,53 @@ class PicacgComicPage extends BaseComicPage { @override bool get isLiked => data!.isLiked; + + @override + Future loadLocalData() async { + String title = '', cover = '', subTitle = ''; + bool found = false; + final h = pageHistory; + if (h != null && h.title.isNotEmpty) { + title = h.title; cover = h.cover; subTitle = h.subtitle; found = true; + } + if (!found) { + try { + final dlId = DownloadManager().generateId(sourceKey, id); + if (DownloadManager().isExists(dlId)) { + final dl = await DownloadManager().getComicOrNull(dlId); + if (dl != null && dl.name.isNotEmpty) { + title = dl.name; subTitle = dl.subTitle; + cover = 'file://${DownloadManager().path}/${DownloadManager().getDirectory(dlId)}/cover.jpg'; + found = true; + } + } + } catch (_) {} + } + if (!found) { + try { + final folders = await LocalFavoritesManager().find(id, const FavoriteType(0)); + if (folders.isNotEmpty) { + final item = LocalFavoritesManager().getComic(folders.first, id, const FavoriteType(0)); + title = item.name; subTitle = item.author; cover = item.coverPath; found = true; + } + } catch (_) {} + } + if (!found) return null; + try { + return ComicItem.fromJson({ + "creator": {"id": "", "title": "", "email": "", "name": "", + "level": 0, "exp": 0, "avatarUrl": "", + "frameUrl": null, "isPunched": null, "slogan": null}, + "id": id, "title": title, "thumbUrl": cover, + "description": "", "author": subTitle, "chineseTeam": "", + "categories": [], "tags": [], + "likes": 0, "comments": 0, "isLiked": false, "isFavourite": false, + "epsCount": 0, "time": "0000-00-00 00:00:00", "pagesCount": 0 + }); + } catch (_) { return null; } + } + @override void openFavoritePanel() { favoriteComic(FavoriteComicWidget(