diff --git a/assets/translation.json b/assets/translation.json index 031c63d..d03fb34 100644 --- a/assets/translation.json +++ b/assets/translation.json @@ -244,7 +244,8 @@ "Deleted @a favorite items.": "已删除 @a 条无效收藏", "New version available": "有新版本可用", "A new version is available. Do you want to update now?" : "有新版本可用。您要现在更新吗?", - "No new version available": "没有新版本可用" + "No new version available": "没有新版本可用", + "Export as pdf": "导出为pdf" }, "zh_TW": { "Home": "首頁", @@ -491,6 +492,7 @@ "Deleted @a favorite items.": "已刪除 @a 條無效收藏", "New version available": "有新版本可用", "A new version is available. Do you want to update now?" : "有新版本可用。您要現在更新嗎?", - "No new version available": "沒有新版本可用" + "No new version available": "沒有新版本可用", + "Export as pdf": "匯出為pdf" } } \ No newline at end of file diff --git a/lib/foundation/local.dart b/lib/foundation/local.dart index 6558df9..1759102 100644 --- a/lib/foundation/local.dart +++ b/lib/foundation/local.dart @@ -76,7 +76,7 @@ class LocalComic with HistoryMixin implements Comic { cover, )); - String get baseDir => directory.contains("/") ? directory : FilePath.join(LocalManager().path, directory); + String get baseDir => (directory.contains('/') || directory.contains('\\')) ? directory : FilePath.join(LocalManager().path, directory); @override String get description => ""; diff --git a/lib/pages/local_comics_page.dart b/lib/pages/local_comics_page.dart index 5c5aa21..a679dfe 100644 --- a/lib/pages/local_comics_page.dart +++ b/lib/pages/local_comics_page.dart @@ -4,9 +4,11 @@ import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/appdata.dart'; import 'package:venera/foundation/comic_source/comic_source.dart'; 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/io.dart'; +import 'package:venera/utils/pdf.dart'; import 'package:venera/utils/translations.dart'; class LocalComicsPage extends StatefulWidget { @@ -299,8 +301,7 @@ class _LocalComicsPageState extends State { return ContentDialog( title: "Delete".tl, content: CheckboxListTile( - title: - Text("Also remove files on disk".tl), + title: Text("Also remove files on disk".tl), value: removeComicFile, onChanged: (v) { state(() { @@ -361,6 +362,34 @@ class _LocalComicsPageState extends State { } controller.close(); }), + if (!multiSelectMode) + MenuEntry( + icon: Icons.picture_as_pdf_outlined, + text: "Export as pdf".tl, + onClick: () async { + var cache = FilePath.join(App.cachePath, 'temp.pdf'); + var controller = showLoadingDialog( + context, + allowCancel: false, + ); + try { + await createPdfFromComicIsolate( + comic: c as LocalComic, + savePath: cache, + ); + await saveFile( + file: File(cache), + filename: "${c.title}.pdf", + ); + } catch (e, s) { + Log.error("PDF Export", e, s); + context.showMessage(message: e.toString()); + } finally { + controller.close(); + File(cache).deleteIgnoreError(); + } + }, + ) ]; }, ), diff --git a/lib/utils/io.dart b/lib/utils/io.dart index cb167ae..6cee959 100644 --- a/lib/utils/io.dart +++ b/lib/utils/io.dart @@ -360,8 +360,8 @@ class _IOOverrides extends IOOverrides { } } -void overrideIO(void Function() f) { - IOOverrides.runWithIOOverrides( +T overrideIO(T Function() f) { + return IOOverrides.runWithIOOverrides( f, _IOOverrides(), ); diff --git a/lib/utils/pdf.dart b/lib/utils/pdf.dart new file mode 100644 index 0000000..95c8e2b --- /dev/null +++ b/lib/utils/pdf.dart @@ -0,0 +1,92 @@ +import 'dart:isolate'; + +import 'package:pdf/widgets.dart'; +import 'package:venera/foundation/local.dart'; +import 'package:venera/utils/io.dart'; + +Future _createPdfFromComic({ + required LocalComic comic, + required String savePath, + required String localPath, +}) async { + final pdf = Document( + title: comic.title, + author: comic.subTitle ?? "", + producer: "Venera", + ); + + pdf.document.outline; + + var baseDir = comic.directory.contains('/') || comic.directory.contains('\\') + ? comic.directory + : FilePath.join(localPath, comic.directory); + + // add cover + var imageData = File(FilePath.join(baseDir, comic.cover)).readAsBytesSync(); + pdf.addPage(Page( + build: (Context context) { + return Image(MemoryImage(imageData), fit: BoxFit.contain); + }, + )); + + bool multiChapters = comic.chapters != null; + + void reorderFiles(List files) { + files.removeWhere( + (element) => element is! File || element.path.startsWith('cover')); + files.sort((a, b) { + var aName = (a as File).name; + var bName = (b as File).name; + var aNumber = int.tryParse(aName); + var bNumber = int.tryParse(bName); + if (aNumber != null && bNumber != null) { + return aNumber.compareTo(bNumber); + } + return aName.compareTo(bName); + }); + } + + if (!multiChapters) { + var files = Directory(baseDir).listSync(); + reorderFiles(files); + + for (var file in files) { + var imageData = (file as File).readAsBytesSync(); + pdf.addPage(Page( + build: (Context context) { + return Image(MemoryImage(imageData), fit: BoxFit.contain); + }, + )); + } + } else { + for (var chapter in comic.chapters!.keys) { + var files = Directory(FilePath.join(baseDir, chapter)).listSync(); + reorderFiles(files); + for (var file in files) { + var imageData = (file as File).readAsBytesSync(); + pdf.addPage(Page( + build: (Context context) { + return Image(MemoryImage(imageData), fit: BoxFit.contain); + }, + )); + } + } + } + + final file = File(savePath); + file.writeAsBytesSync(await pdf.save()); +} + +Future createPdfFromComicIsolate({ + required LocalComic comic, + required String savePath, +}) async { + var localPath = LocalManager().path; + return Isolate.run(() => overrideIO(() async { + return await _createPdfFromComic( + comic: comic, + savePath: savePath, + localPath: localPath, + ); + })); +} diff --git a/pubspec.lock b/pubspec.lock index 8128abe..167bbaa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -33,6 +33,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + archive: + dependency: transitive + description: + name: archive + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d + url: "https://pub.dev" + source: hosted + version: "3.6.1" args: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + barcode: + dependency: transitive + description: + name: barcode + sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003 + url: "https://pub.dev" + source: hosted + version: "2.2.8" battery_plus: dependency: "direct main" description: @@ -65,6 +81,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + bidi: + dependency: transitive + description: + name: bidi + sha256: "9a712c7ddf708f7c41b1923aa83648a3ed44cfd75b04f72d598c45e5be287f9d" + url: "https://pub.dev" + source: hosted + version: "2.0.12" boolean_selector: dependency: transitive description: @@ -465,6 +489,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d + url: "https://pub.dev" + source: hosted + version: "4.3.0" intl: dependency: "direct main" description: @@ -626,6 +658,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.0" + path_parsing: + dependency: transitive + description: + name: path_parsing + sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca" + url: "https://pub.dev" + source: hosted + version: "1.1.0" path_provider: dependency: "direct main" description: @@ -674,6 +714,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + pdf: + dependency: "direct main" + description: + name: pdf + sha256: "05df53f8791587402493ac97b9869d3824eccbc77d97855f4545cf72df3cae07" + url: "https://pub.dev" + source: hosted + version: "3.11.1" petitparser: dependency: transitive description: @@ -715,6 +763,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.9.1" + qr: + dependency: transitive + description: + name: qr + sha256: "5a1d2586170e172b8a8c8470bbbffd5eb0cd38a66c0d77155ea138d3af3a4445" + url: "https://pub.dev" + source: hosted + version: "3.0.2" rhttp: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index f20cec6..713a57c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: git: url: https://github.com/pkuislm/flutter_saf.git ref: dd5242918da0ea9a0a50b0f87ade7a2def65453d + pdf: ^3.11.1 dev_dependencies: flutter_test: