From 617c452e0715ecedbe6992683dfb8c1c1918cf91 Mon Sep 17 00:00:00 2001 From: nyne Date: Sat, 7 Dec 2024 20:04:22 +0800 Subject: [PATCH] fix #90: export comic as epub --- assets/translation.json | 6 +- lib/components/message.dart | 39 ++-- lib/pages/favorites/favorite_actions.dart | 2 - lib/pages/local_comics_page.dart | 28 +++ lib/utils/epub.dart | 208 ++++++++++++++++++++++ 5 files changed, 263 insertions(+), 20 deletions(-) create mode 100644 lib/utils/epub.dart diff --git a/assets/translation.json b/assets/translation.json index 985ab59..1114ff3 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -246,7 +246,8 @@ "New version available": "有新版本可用", "A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?", "No new version available": "没有新版本可用", - "Export as pdf": "导出为pdf" + "Export as pdf": "导出为pdf", + "Export as epub": "导出为epub" }, "zh_TW": { "Home": "首頁", @@ -495,6 +496,7 @@ "New version available": "有新版本可用", "A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?", "No new version available": "沒有新版本可用", - "Export as pdf": "匯出為pdf" + "Export as pdf": "匯出為pdf", + "Export as epub": "匯出為epub" } } \ No newline at end of file diff --git a/lib/components/message.dart b/lib/components/message.dart index 0024956..3618e40 100644 --- a/lib/components/message.dart +++ b/lib/components/message.dart @@ -46,21 +46,28 @@ class _ToastOverlay extends StatelessWidget { child: IconTheme( data: IconThemeData( color: Theme.of(context).colorScheme.onInverseSurface), - child: Container( - padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - if (icon != null) icon!.paddingRight(8), - Text( - message, - style: const TextStyle( - fontSize: 16, fontWeight: FontWeight.w500), - maxLines: 3, - overflow: TextOverflow.ellipsis, - ), - if (trailing != null) trailing!.paddingLeft(8) - ], + child: IntrinsicWidth( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 16), + constraints: BoxConstraints( + maxWidth: context.width - 32, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (icon != null) icon!.paddingRight(8), + Expanded( + child: Text( + message, + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w500), + maxLines: 3, + overflow: TextOverflow.ellipsis, + ), + ), + if (trailing != null) trailing!.paddingLeft(8) + ], + ), ), ), ), @@ -220,7 +227,7 @@ LoadingDialogController showLoadingDialog(BuildContext context, ); }); - var navigator = Navigator.of(context); + var navigator = Navigator.of(context, rootNavigator: true); navigator.push(loadingDialogRoute).then((value) => controller.closed = true); diff --git a/lib/pages/favorites/favorite_actions.dart b/lib/pages/favorites/favorite_actions.dart index c6f494a..23f13f0 100644 --- a/lib/pages/favorites/favorite_actions.dart +++ b/lib/pages/favorites/favorite_actions.dart @@ -1,5 +1,3 @@ -import 'package:venera/foundation/appdata.dart'; - part of 'favorites_page.dart'; /// Open a dialog to create a new favorite folder. diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index a679dfe..cbbc979 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -7,6 +7,7 @@ import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/log.dart'; import 'package:venera/pages/downloading_page.dart'; import 'package:venera/utils/cbz.dart'; +import 'package:venera/utils/epub.dart'; import 'package:venera/utils/io.dart'; import 'package:venera/utils/pdf.dart'; import 'package:venera/utils/translations.dart'; @@ -389,6 +390,33 @@ class _LocalComicsPageState extends State { File(cache).deleteIgnoreError(); } }, + ), + if (!multiSelectMode) + MenuEntry( + icon: Icons.import_contacts_outlined, + text: "Export as epub".tl, + onClick: () async { + var controller = showLoadingDialog( + context, + allowCancel: false, + ); + File? file; + try { + file = await createEpubWithLocalComic( + c as LocalComic, + ); + await saveFile( + file: file, + filename: "${c.title}.epub", + ); + } catch (e, s) { + Log.error("EPUB Export", e, s); + context.showMessage(message: e.toString()); + } finally { + controller.close(); + file?.deleteIgnoreError(); + } + }, ) ]; }, diff --git a/lib/utils/epub.dart b/lib/utils/epub.dart new file mode 100644 index 0000000..ed5ae98 --- /dev/null +++ b/lib/utils/epub.dart @@ -0,0 +1,208 @@ +import 'dart:isolate'; + +import 'package:uuid/uuid.dart'; +import 'package:venera/foundation/app.dart'; +import 'package:venera/foundation/local.dart'; +import 'package:venera/utils/file_type.dart'; +import 'package:venera/utils/io.dart'; +import 'package:zip_flutter/zip_flutter.dart'; + +class EpubData { + final String title; + + final String author; + + final File cover; + + final Map> chapters; + + const EpubData({ + required this.title, + required this.author, + required this.cover, + required this.chapters, + }); +} + +Future createEpubComic(EpubData data, String cacheDir) async { + final workingDir = Directory(FilePath.join(cacheDir, 'epub')); + if (workingDir.existsSync()) { + workingDir.deleteSync(recursive: true); + } + workingDir.createSync(recursive: true); + + // mimetype + workingDir.joinFile('mimetype').writeAsStringSync('application/epub+zip'); + + // META-INF + Directory(FilePath.join(workingDir.path, 'META-INF')).createSync(); + File(FilePath.join(workingDir.path, 'META-INF', 'container.xml')) + .writeAsStringSync(''' + + + + + + + '''); + + Directory(FilePath.join(workingDir.path, 'OEBPS')).createSync(); + + // copy images, create html files + final imageDir = Directory(FilePath.join(workingDir.path, 'OEBPS', 'images')); + imageDir.createSync(); + final coverExt = data.cover.extension; + final coverMime = FileType.fromExtension(coverExt).mime; + imageDir + .joinFile('cover.$coverExt') + .writeAsBytesSync(data.cover.readAsBytesSync()); + int imgIndex = 0; + int chapterIndex = 0; + var manifestStrBuilder = StringBuffer(); + manifestStrBuilder.writeln( + ' '); + manifestStrBuilder.writeln( + ' '); + for (final chapter in data.chapters.keys) { + var images = []; + for (final image in data.chapters[chapter]!) { + final ext = image.extension; + imageDir + .joinFile('img$imgIndex.$ext') + .writeAsBytesSync(image.readAsBytesSync()); + images.add('images/img$imgIndex.$ext'); + var mime = FileType.fromExtension(ext).mime; + manifestStrBuilder.writeln( + ' '); + imgIndex++; + } + var html = + File(FilePath.join(workingDir.path, 'OEBPS', '$chapterIndex.html')); + html.writeAsStringSync(''' + + + + $chapter + + + +

$chapter

+
+${images.map((e) => ' $e').join('\n')} +
+ + + '''); + manifestStrBuilder.writeln( + ' '); + chapterIndex++; + } + + // content.opf + final contentOpf = + File(FilePath.join(workingDir.path, 'content.opf')); + final uuid = const Uuid().v4(); + var spineStrBuilder = StringBuffer(); + for (var i = 0; i < chapterIndex; i++) { + var idRef = 'idref="chapter$i"'; + spineStrBuilder.writeln(' '); + } + contentOpf.writeAsStringSync(''' + + + + ${data.title} + ${data.author} + urn:uuid:$uuid + + + +${manifestStrBuilder.toString()} + + +${spineStrBuilder.toString()} + + + '''); + + // toc.ncx + final tocNcx = File(FilePath.join(workingDir.path, 'toc.ncx')); + var navMapStrBuilder = StringBuffer(); + var playOrder = 2; + final chapterNames = data.chapters.keys.toList(); + for (var i = 0; i < chapterIndex; i++) { + navMapStrBuilder + .writeln(' '); + navMapStrBuilder.writeln( + ' ${chapterNames[i]}'); + navMapStrBuilder.writeln(' '); + navMapStrBuilder.writeln(' '); + playOrder++; + } + + tocNcx.writeAsStringSync(''' + + + + + + + + + + + ${data.title} + + +${navMapStrBuilder.toString()} + + + '''); + + // zip + final zipPath = FilePath.join(cacheDir, '${data.title}.epub'); + ZipFile.compressFolder(workingDir.path, zipPath); + + return File(zipPath); +} + +Future createEpubWithLocalComic(LocalComic comic) async { + var chapters = >{}; + if (comic.chapters == null) { + chapters[comic.title] = + (await LocalManager().getImages(comic.id, comic.comicType, 0)) + .map((e) => File(e)) + .toList(); + } else { + for (var chapter in comic.chapters!.keys) { + chapters[comic.chapters![chapter]!] = (await LocalManager() + .getImages(comic.id, comic.comicType, chapter)) + .map((e) => File(e)) + .toList(); + } + } + var data = EpubData( + title: comic.title, + author: comic.subtitle, + cover: comic.coverFile, + chapters: chapters, + ); + + final cacheDir = App.cachePath; + + return Isolate.run(() => overrideIO(() async { + return createEpubComic(data, cacheDir); + })); +}