support 7z;

fix #137
This commit is contained in:
2025-01-17 22:30:25 +08:00
parent bfd115046d
commit 5f36ef6ea3
7 changed files with 70 additions and 22 deletions

View File

@@ -154,8 +154,8 @@
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。", "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目录包含一个名为'cover.*'的文件,它将被用作封面图片。否则将使用第一张图片。",
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n", "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目录名称将被用作漫画标题。章节目录的名称将被用作章节标题。\n",
"Export as cbz": "导出为cbz", "Export as cbz": "导出为cbz",
"Select a cbz/zip file." : "选择一个cbz/zip文件", "Select an archive file (cbz, zip, 7z, cb7)" : "选择一个归档文件 (cbz, zip, 7z, cb7)",
"A cbz file" : "一个cbz文件", "An archive file" : "一个归档文件",
"Fullscreen": "全屏", "Fullscreen": "全屏",
"Exit": "退出", "Exit": "退出",
"View more": "查看更多", "View more": "查看更多",
@@ -297,8 +297,8 @@
"End": "末尾", "End": "末尾",
"None": "无", "None": "无",
"View Detail": "查看详情", "View Detail": "查看详情",
"Select a directory which contains multiple cbz/zip files." : "选择一个包含多个cbz/zip文件的目录", "Select a directory which contains multiple archive files." : "选择一个包含多个归档文件的目录",
"Multiple cbz files" : "多个cbz文件", "Multiple archive files" : "多个归档文件",
"No valid comics found" : "未找到有效的漫画", "No valid comics found" : "未找到有效的漫画",
"Enable DNS Overrides": "启用DNS覆写", "Enable DNS Overrides": "启用DNS覆写",
"DNS Overrides": "DNS覆写", "DNS Overrides": "DNS覆写",
@@ -314,7 +314,8 @@
"Reset": "重置", "Reset": "重置",
"Tags": "标签", "Tags": "标签",
"Authors": "作者", "Authors": "作者",
"Comics": "漫画" "Comics": "漫画",
"Imported @a comics": "已导入 @a 本漫画"
}, },
"zh_TW": { "zh_TW": {
"Home": "首頁", "Home": "首頁",
@@ -470,8 +471,8 @@
"If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。", "If the directory contains a file named 'cover.*', it will be used as the cover image. Otherwise the first image will be used." : "如果目錄包含一個名為'cover.*'的文件,它將被用作封面圖片。否則將使用第一張圖片。",
"The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n", "The directory name will be used as the comic title. And the name of chapter directories will be used as the chapter titles.\n" : "目錄名稱將被用作漫畫標題。章節目錄的名稱將被用作章節標題。\n",
"Export as cbz": "匯出為cbz", "Export as cbz": "匯出為cbz",
"Select a cbz/zip file." : "選擇一個cbz/zip文件", "Select an archive file (cbz, zip, 7z, cb7)" : "選擇一個歸檔文件 (cbz, zip, 7z, cb7)",
"A cbz file" : "一個cbz文件", "An archive file" : "一個歸檔文件",
"Fullscreen": "全螢幕", "Fullscreen": "全螢幕",
"Exit": "退出", "Exit": "退出",
"View more": "查看更多", "View more": "查看更多",
@@ -614,8 +615,8 @@
"End": "末尾", "End": "末尾",
"None": "無", "None": "無",
"View Detail": "查看詳情", "View Detail": "查看詳情",
"Select a directory which contains multiple cbz/zip files." : "選擇一個包含多個cbz/zip文件的目錄", "Select a directory which contains multiple archive files." : "選擇一個包含多個歸檔文件的目錄",
"Multiple cbz files" : "多個cbz文件", "Multiple archive files" : "多個歸檔文件",
"No valid comics found" : "未找到有效的漫畫", "No valid comics found" : "未找到有效的漫畫",
"Enable DNS Overrides": "啟用DNS覆寫", "Enable DNS Overrides": "啟用DNS覆寫",
"DNS Overrides": "DNS覆寫", "DNS Overrides": "DNS覆寫",
@@ -631,6 +632,7 @@
"Reset": "重置", "Reset": "重置",
"Tags": "標籤", "Tags": "標籤",
"Authors": "作者", "Authors": "作者",
"Comics": "漫畫" "Comics": "漫畫",
"Imported @a comics": "已匯入 @a 部漫畫"
} }
} }

View File

@@ -453,15 +453,15 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
String info = [ String info = [
"Select a directory which contains the comic files.".tl, "Select a directory which contains the comic files.".tl,
"Select a directory which contains the comic directories.".tl, "Select a directory which contains the comic directories.".tl,
"Select a cbz/zip file.".tl, "Select an archive file (cbz, zip, 7z, cb7)".tl,
"Select a directory which contains multiple cbz/zip files.".tl, "Select a directory which contains multiple archive files.".tl,
"Select an EhViewer database and a download folder.".tl "Select an EhViewer database and a download folder.".tl
][type]; ][type];
List<String> importMethods = [ List<String> importMethods = [
"Single Comic".tl, "Single Comic".tl,
"Multiple Comics".tl, "Multiple Comics".tl,
"A cbz file".tl, "An archive file".tl,
"Multiple cbz files".tl, "Multiple archive files".tl,
"EhViewer downloads".tl "EhViewer downloads".tl
]; ];
@@ -493,7 +493,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
}, },
); );
}), }),
if (type != 3) if (type != 4)
ListTile( ListTile(
title: Text("Add to favorites".tl), title: Text("Add to favorites".tl),
trailing: Select( trailing: Select(
@@ -507,7 +507,7 @@ class _ImportComicsWidgetState extends State<_ImportComicsWidget> {
}, },
), ),
).paddingHorizontal(8), ).paddingHorizontal(8),
if (!App.isIOS && !App.isMacOS) if (!App.isIOS && !App.isMacOS && type != 2 && type != 3)
CheckboxListTile( CheckboxListTile(
enabled: true, enabled: true,
title: Text("Copy to app local path".tl), title: Text("Copy to app local path".tl),

View File

@@ -1,9 +1,10 @@
import 'dart:convert'; import 'dart:convert';
import 'package:flutter_7zip/flutter_7zip.dart';
import 'package:venera/foundation/app.dart'; import 'package:venera/foundation/app.dart';
import 'package:venera/foundation/comic_type.dart'; import 'package:venera/foundation/comic_type.dart';
import 'package:venera/foundation/local.dart'; import 'package:venera/foundation/local.dart';
import 'package:venera/utils/ext.dart'; import 'package:venera/utils/ext.dart';
import 'package:venera/utils/file_type.dart';
import 'package:venera/utils/io.dart'; import 'package:venera/utils/io.dart';
import 'package:zip_flutter/zip_flutter.dart'; import 'package:zip_flutter/zip_flutter.dart';
@@ -57,12 +58,33 @@ class ComicChapter {
ComicChapter({required this.title, required this.start, required this.end}); ComicChapter({required this.title, required this.start, required this.end});
} }
/// Comic Book Archive. Currently supports CBZ, ZIP and 7Z formats.
abstract class CBZ { abstract class CBZ {
static Future<FileType> checkType(File file) async {
var header = <int>[];
await for (var bytes in file.openRead()) {
header.addAll(bytes);
if (header.length >= 32) break;
}
return detectFileType(header);
}
static Future<void> 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<LocalComic> import(File file) async { static Future<LocalComic> import(File file) async {
var cache = Directory(FilePath.join(App.cachePath, 'cbz_import')); var cache = Directory(FilePath.join(App.cachePath, 'cbz_import'));
if (cache.existsSync()) cache.deleteSync(recursive: true); if (cache.existsSync()) cache.deleteSync(recursive: true);
cache.createSync(); cache.createSync();
await ZipFile.openAndExtractAsync(file.path, cache.path, 4); await extractArchive(file, cache);
var metaDataFile = File(FilePath.join(cache.path, 'metadata.json')); var metaDataFile = File(FilePath.join(cache.path, 'metadata.json'));
ComicMetaData? metaData; ComicMetaData? metaData;
if (metaDataFile.existsSync()) { if (metaDataFile.existsSync()) {
@@ -72,7 +94,7 @@ abstract class CBZ {
} catch (_) {} } catch (_) {}
} }
metaData ??= ComicMetaData( metaData ??= ComicMetaData(
title: file.name.replaceLast('.cbz', ''), title: file.name.substring(0, file.name.lastIndexOf('.')),
author: "", author: "",
tags: [], tags: [],
); );
@@ -86,6 +108,7 @@ abstract class CBZ {
return !['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'].contains(ext); return !['jpg', 'jpeg', 'png', 'webp', 'gif', 'jpe'].contains(ext);
}); });
if(files.isEmpty) { if(files.isEmpty) {
cache.deleteSync(recursive: true);
throw Exception('No images found in the archive'); throw Exception('No images found in the archive');
} }
files.sort((a, b) => a.path.compareTo(b.path)); files.sort((a, b) => a.path.compareTo(b.path));

View File

@@ -21,8 +21,17 @@ class FileType {
} }
} }
final _resolver = MimeTypeResolver()
// zip
..addMagicNumber([0x50, 0x4B], 'application/zip')
// 7z
..addMagicNumber([0x37, 0x7A, 0xBC, 0xAF, 0x27, 0x1C], 'application/x-7z-compressed')
// rar
..addMagicNumber([0x52, 0x61, 0x72, 0x21, 0x1A, 0x07], 'application/vnd.rar')
;
FileType detectFileType(List<int> data) { FileType detectFileType(List<int> data) {
var mime = lookupMimeType('no-file', headerBytes: data); var mime = _resolver.lookup('no-file', headerBytes: data);
var ext = mime == null ? '' : extensionFromMime(mime); var ext = mime == null ? '' : extensionFromMime(mime);
if(ext == 'jpe') { if(ext == 'jpe') {
ext = 'jpg'; ext = 'jpg';

View File

@@ -20,7 +20,7 @@ class ImportComic {
const ImportComic({this.selectedFolder, this.copyToLocal = true}); const ImportComic({this.selectedFolder, this.copyToLocal = true});
Future<bool> cbz() async { Future<bool> cbz() async {
var file = await selectFile(ext: ['cbz', 'zip']); var file = await selectFile(ext: ['cbz', 'zip', '7z', 'cb7']);
Map<String?, List<LocalComic>> imported = {}; Map<String?, List<LocalComic>> imported = {};
if (file == null) { if (file == null) {
return false; return false;
@@ -42,7 +42,8 @@ class ImportComic {
var dir = await picker.pickDirectory(directAccess: true); var dir = await picker.pickDirectory(directAccess: true);
if (dir != null) { if (dir != null) {
var files = (await dir.list().toList()).whereType<File>().toList(); var files = (await dir.list().toList()).whereType<File>().toList();
files.removeWhere((e) => e.extension != 'cbz' && e.extension != 'zip'); const supportedExtensions = ['cbz', 'zip', '7z', 'cb7'];
files.removeWhere((e) => !supportedExtensions.contains(e.extension));
Map<String?, List<LocalComic>> imported = {}; Map<String?, List<LocalComic>> imported = {};
var controller = showLoadingDialog(App.rootContext, allowCancel: false); var controller = showLoadingDialog(App.rootContext, allowCancel: false);
var comics = <LocalComic>[]; var comics = <LocalComic>[];

View File

@@ -303,6 +303,15 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_7zip:
dependency: "direct main"
description:
path: "."
ref: "841ef5f77e5fdfd79e3eb2fa07ece4d46787285b"
resolved-ref: "841ef5f77e5fdfd79e3eb2fa07ece4d46787285b"
url: "https://github.com/wgh136/flutter_7zip"
source: git
version: "0.0.1"
flutter_file_dialog: flutter_file_dialog:
dependency: "direct main" dependency: "direct main"
description: description:

View File

@@ -73,6 +73,10 @@ dependencies:
flutter_memory_info: ^0.0.1 flutter_memory_info: ^0.0.1
syntax_highlight: ^0.4.0 syntax_highlight: ^0.4.0
text_scroll: ^0.2.0 text_scroll: ^0.2.0
flutter_7zip:
git:
url: https://github.com/wgh136/flutter_7zip
ref: 841ef5f77e5fdfd79e3eb2fa07ece4d46787285b
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: