From bd5d10e91989cfc790e05ea89ac783142b847924 Mon Sep 17 00:00:00 2001 From: nyne Date: Thu, 20 Feb 2025 13:08:55 +0800 Subject: [PATCH] Improve comic chapters. --- lib/components/appbar.dart | 1 + lib/foundation/comic_source/models.dart | 160 ++++++++++++---- .../image_favorites_provider.dart | 2 +- lib/foundation/local.dart | 10 +- lib/network/download.dart | 5 +- lib/pages/comic_details_page/actions.dart | 24 +-- lib/pages/comic_details_page/chapters.dart | 18 +- lib/pages/comic_details_page/comic_page.dart | 4 +- lib/pages/reader/images.dart | 2 +- lib/pages/reader/loading.dart | 2 +- lib/pages/reader/reader.dart | 6 +- lib/pages/reader/scaffold.dart | 179 ++++++++++++++++-- lib/utils/cbz.dart | 3 +- lib/utils/import_comic.dart | 5 +- 14 files changed, 324 insertions(+), 97 deletions(-) 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/comic_source/models.dart b/lib/foundation/comic_source/models.dart index 8e39b46..9d439bb 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 + Map getGroupByIndex(int index) { + return _groupedChapters!.values.elementAt(index); + } + + /// Get a chapter by index + 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/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..4aa516c 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(), }; } @@ -391,7 +391,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 +425,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 = []; diff --git a/lib/network/download.dart b/lib/network/download.dart index 058b93e..76a69f9 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; } diff --git a/lib/pages/comic_details_page/actions.dart b/lib/pages/comic_details_page/actions.dart index 7636426..da10543 100644 --- a/lib/pages/comic_details_page/actions.dart +++ b/lib/pages/comic_details_page/actions.dart @@ -98,7 +98,7 @@ abstract mixin class _ComicPageActions { void read([int? ep, int? page]) { App.rootContext .to( - () => Reader( + () => Reader( type: comic.comicType, cid: comic.id, name: comic.title, @@ -219,7 +219,7 @@ abstract mixin class _ComicPageActions { isGettingLink = true; }); var res = - await comicSource.archiveDownloader!.getDownloadUrl( + await comicSource.archiveDownloader!.getDownloadUrl( comic.id, archives![selected].id, ); @@ -262,7 +262,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 +270,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 +281,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 +298,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 +432,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..2f13089 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,12 +279,12 @@ 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; } bool visited = (history?.readEpisode ?? {}).contains(chapterIndex + 1); diff --git a/lib/pages/comic_details_page/comic_page.dart b/lib/pages/comic_details_page/comic_page.dart index 609246c..de3a662 100644 --- a/lib/pages/comic_details_page/comic_page.dart +++ b/lib/pages/comic_details_page/comic_page.dart @@ -386,7 +386,7 @@ class _ComicPageState extends LoadingState String text; if (haveChapter) { text = "Last Reading: @epName Page @page".tlParams({ - 'epName': comic.chapters!.values.elementAt( + 'epName': comic.chapters!.titles.elementAt( math.min(ep - 1, comic.chapters!.length - 1)), 'page': page, }); @@ -610,7 +610,7 @@ class _ComicPageState extends LoadingState } return _ComicChapters( history: history, - groupedMode: comic.groupedChapters != null, + groupedMode: comic.chapters!.isGrouped, ); } diff --git a/lib/pages/reader/images.dart b/lib/pages/reader/images.dart index 0091f01..a90382b 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(() { diff --git a/lib/pages/reader/loading.dart b/lib/pages/reader/loading.dart index 38bfd57..e8a09d4 100644 --- a/lib/pages/reader/loading.dart +++ b/lib/pages/reader/loading.dart @@ -101,7 +101,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 609c9e0..4e3d139 100644 --- a/lib/pages/reader/reader.dart +++ b/lib/pages/reader/reader.dart @@ -76,9 +76,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; @@ -105,7 +103,7 @@ 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; diff --git a/lib/pages/reader/scaffold.dart b/lib/pages/reader/scaffold.dart index 453081b..a60cdb3 100644 --- a/lib/pages/reader/scaffold.dart +++ b/lib/pages/reader/scaffold.dart @@ -279,7 +279,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 +561,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 +614,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, ); } @@ -1030,14 +1032,27 @@ class _ChaptersView extends StatefulWidget { class _ChaptersViewState extends State<_ChaptersView> { bool desc = false; + late final ScrollController _scrollController; + + @override + void initState() { + super.initState(); + int epIndex = widget.reader.chapter - 2; + _scrollController = ScrollController( + initialScrollOffset: (epIndex * 48.0 + 52).clamp(0, double.infinity), + ); + } + @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( @@ -1063,26 +1078,35 @@ class _ChaptersViewState extends State<_ChaptersView> { 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, - ), + var chapter = chapters.titles.elementAt(index); + return InkWell( onTap: () { widget.reader.toChapter(index + 1); Navigator.of(context).pop(); }, + child: Container( + height: 48, + padding: const EdgeInsets.only(left: 16), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: current == index + ? context.colorScheme.primary + : Colors.transparent, + width: 2, + ), + ), + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + chapter, + style: current == index + ? ts.withColor(context.colorScheme.primary).bold.s16 + : ts.s16, + ), + ), + ), ); }, childCount: chapters.length, @@ -1093,3 +1117,120 @@ class _ChaptersViewState extends State<_ChaptersView> { ); } } + +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 initialGroupName; + + @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), + ); + } + + @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 InkWell( + onTap: () { + widget.reader.toChapter(i); + context.pop(); + }, + child: Container( + height: 48, + padding: const EdgeInsets.only(left: 16), + decoration: BoxDecoration( + border: Border( + left: BorderSide( + color: widget.reader.chapter == i + ? context.colorScheme.primary + : Colors.transparent, + width: 2, + ), + ), + ), + child: Align( + alignment: Alignment.centerLeft, + child: Text( + name, + style: widget.reader.chapter == i + ? ts.withColor(context.colorScheme.primary).bold.s16 + : ts.s16, + ), + ), + ), + ); + }, + childCount: group.length, + ), + ), + ], + ); + } +} 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/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,