import 'dart:convert'; import 'package:flutter_7zip/flutter_7zip.dart'; import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/local.dart'; import 'package:venera/utils/ext.dart'; import 'package:venera/utils/file_type.dart'; import 'package:venera/utils/io.dart'; import 'package:zip_flutter/zip_flutter.dart'; class ComicMetaData { final String title; final String author; final List tags; final List? chapters; Map toJson() => { 'title': title, 'author': author, 'tags': tags, 'chapters': chapters?.map((e) => e.toJson()).toList() }; ComicMetaData.fromJson(Map json) : title = json['title'], author = json['author'], tags = List.from(json['tags']), chapters = json['chapters'] == null ? null : List.from( json['chapters'].map((e) => ComicChapter.fromJson(e))); ComicMetaData({ required this.title, required this.author, required this.tags, this.chapters, }); } class ComicChapter { final String title; final int start; final int end; Map toJson() => {'title': title, 'start': start, 'end': end}; ComicChapter.fromJson(Map json) : title = json['title'], start = json['start'], end = json['end']; ComicChapter({required this.title, required this.start, required this.end}); } /// Comic Book Archive. Currently supports CBZ, ZIP and 7Z formats. abstract class CBZ { static Future checkType(File file) async { var header = []; await for (var bytes in file.openRead()) { header.addAll(bytes); if (header.length >= 32) break; } return detectFileType(header); } static Future extractArchive(File file, Directory out) async { var fileType = await checkType(file); if (fileType.mime == 'application/zip') { await ZipFile.openAndExtractAsync(file.path, out.path, 4); } else if (fileType.mime == "application/x-7z-compressed") { await SZArchive.extractIsolates(file.path, out.path, 4); } else { throw Exception('Unsupported archive type'); } } static Future import(File file) async { var cache = Directory(FilePath.join(App.cachePath, 'cbz_import')); if (cache.existsSync()) cache.deleteSync(recursive: true); cache.createSync(); await extractArchive(file, cache); var f = cache.listSync(); if (f.length == 1 && f.first is Directory) { cache = f.first as Directory; } var metaDataFile = File(FilePath.join(cache.path, 'metadata.json')); ComicMetaData? metaData; if (metaDataFile.existsSync()) { try { metaData = ComicMetaData.fromJson(jsonDecode(metaDataFile.readAsStringSync())); } catch (_) {} } metaData ??= ComicMetaData( title: file.name.substring(0, file.name.lastIndexOf('.')), author: "", tags: [], ); var old = LocalManager().findByName(metaData.title); if (old != null) { throw Exception('Comic with name ${metaData.title} already exists'); } var files = cache.listSync().whereType().toList(); files.removeWhere((e) { var ext = e.path.split('.').last; return !['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'].contains(ext); }); if(files.isEmpty) { cache.deleteSync(recursive: true); throw Exception('No images found in the archive'); } files.sort((a, b) => a.path.compareTo(b.path)); var coverFile = files.firstWhereOrNull( (element) => element.path.endsWith('cover.${element.path.split('.').last}'), ); if (coverFile != null) { files.remove(coverFile); } else { coverFile = files.first; } Map? cpMap; var dest = Directory( FilePath.join(LocalManager().path, sanitizeFileName(metaData.title)), ); dest.createSync(); coverFile.copyMem( FilePath.join(dest.path, 'cover.${coverFile.extension}')); if (metaData.chapters == null) { for (var i = 0; i < files.length; i++) { var src = files[i]; var dst = File( FilePath.join(dest.path, '${i + 1}.${src.path.split('.').last}')); await src.copyMem(dst.path); } } else { dest.createSync(); var chapters = >{}; for (var chapter in metaData.chapters!) { chapters[chapter.title] = files.sublist(chapter.start - 1, chapter.end); } int i = 0; cpMap = {}; for (var chapter in chapters.entries) { cpMap[i.toString()] = chapter.key; var chapterDir = Directory(FilePath.join(dest.path, i.toString())); chapterDir.createSync(); for (var i = 0; i < chapter.value.length; i++) { var src = chapter.value[i]; var dst = File(FilePath.join( chapterDir.path, '${i + 1}.${src.path.split('.').last}')); await src.copyMem(dst.path); } } } var comic = LocalComic( id: LocalManager().findValidId(ComicType.local), title: metaData.title, subtitle: metaData.author, tags: metaData.tags, comicType: ComicType.local, directory: dest.name, chapters: cpMap, downloadedChapters: cpMap?.keys.toList() ?? [], cover: 'cover.${coverFile.extension}', createdAt: DateTime.now(), ); await cache.delete(recursive: true); return comic; } static Future export(LocalComic comic) async { var cache = Directory(FilePath.join(App.cachePath, 'cbz_export')); if (cache.existsSync()) cache.deleteSync(recursive: true); cache.createSync(); List? chapters; if (comic.chapters == null) { var images = await LocalManager().getImages(comic.id, comic.comicType, 1); int i = 1; for (var image in images) { var src = File(image.replaceFirst('file://', '')); var width = images.length.toString().length; var dstName = '${i.toString().padLeft(width, '0')}.${image.split('.').last}'; var dst = File(FilePath.join(cache.path, dstName)); await src.copyMem(dst.path); i++; } } else { chapters = []; var allImages = []; for (var c in comic.downloadedChapters) { var chapterName = comic.chapters![c]; var images = await LocalManager().getImages( comic.id, comic.comicType, c, ); allImages.addAll(images); var chapter = ComicChapter( title: chapterName!, start: chapters.length + 1, end: chapters.length + images.length, ); chapters.add(chapter); } int i = 1; for (var image in allImages) { var src = File(image); var width = allImages.length.toString().length; var dstName = '${i.toString().padLeft(width, '0')}.${image.split('.').last}'; var dst = File(FilePath.join(cache.path, dstName)); await src.copyMem(dst.path); i++; } } var cover = comic.coverFile; await cover .copyMem(FilePath.join(cache.path, 'cover.${cover.path.split('.').last}')); await File(FilePath.join(cache.path, 'metadata.json')).writeAsString( jsonEncode( ComicMetaData( title: comic.title, author: comic.subtitle, tags: comic.tags, chapters: chapters, ).toJson(), ), ); var cbz = File(FilePath.join(App.cachePath, sanitizeFileName('${comic.title}.cbz'))); if (cbz.existsSync()) cbz.deleteSync(); await _compress(cache.path, cbz.path); cache.deleteSync(recursive: true); return cbz; } static _compress(String src, String dst) async { await ZipFile.compressFolderAsync(src, dst, 4); } }