refactor: streamline archive import process and enhance cover image handling

This commit is contained in:
Moustapha Kodjo Amadou 2025-11-11 13:13:35 +01:00
parent 75d5013179
commit fd615bd44b
9 changed files with 432 additions and 274 deletions

View file

@ -23,6 +23,7 @@ import 'package:mangayomi/modules/anime/providers/anime_player_controller_provid
import 'package:mangayomi/modules/anime/widgets/aniskip_countdown_btn.dart';
import 'package:mangayomi/modules/anime/widgets/desktop.dart';
import 'package:mangayomi/modules/anime/widgets/play_or_pause_button.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart';
import 'package:mangayomi/modules/anime/widgets/mobile.dart';
import 'package:mangayomi/modules/anime/widgets/subtitle_view.dart';
@ -2223,7 +2224,8 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
..updatedAt = DateTime.now()
.millisecondsSinceEpoch
..customCoverImage =
imageBytes,
imageBytes
?.getCoverImage,
);
});
if (context.mounted) {

View file

@ -1131,6 +1131,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
// Downloaded Chapters
if (downloadedChapsList.isNotEmpty) {
for (var manga in mangasList) {
if (!(manga.isLocalArchive ?? false)) {
String mangaDirectory = "";
if (manga.isLocalArchive ?? false) {
mangaDirectory = _deleteImport(
@ -1159,6 +1160,7 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
}
}
}
}
ref.read(mangasListStateProvider.notifier).clear();
ref

View file

@ -1,9 +1,11 @@
import 'dart:convert';
import 'dart:io'; // For I/O-operations
import 'dart:typed_data';
import 'package:epubx/epubx.dart';
import 'package:isar_community/isar.dart'; // Isar database package for local storage
import 'package:mangayomi/main.dart'; // Exposes the global `isar` instance
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/utils/extensions/others.dart';
import 'package:path/path.dart' as p; // For manipulating file system paths
import 'package:bot_toast/bot_toast.dart'; // For Exceptions
@ -182,7 +184,7 @@ Future<void> _scanDirectory(Ref ref, Directory? dir) async {
final bytes = await File(imageFiles.first.path).readAsBytes();
final byteList = bytes.toList();
if (manga.customCoverImage != byteList) {
manga.customCoverImage = byteList;
manga.customCoverImage = Uint8List.fromList(byteList).getCoverImage;
manga.lastUpdate = dateNow;
}
} catch (e) {
@ -299,7 +301,9 @@ Future<void> _scanDirectory(Ref ref, Directory? dir) async {
book.Content!.Images!.containsKey("media/file0.png")
? book.Content!.Images!["media/file0.png"]!.Content
: book.Content!.Images!.values.first.Content;
manga.customCoverImage = coverImage;
manga.customCoverImage = coverImage == null
? null
: Uint8List.fromList(coverImage).getCoverImage;
saveManga++;
}
for (var chapter in book.Chapters ?? []) {

View file

@ -17,6 +17,8 @@ Future importArchivesFromFile(
required ItemType itemType,
required bool init,
}) async {
final keepAlile = ref.keepAlive();
try {
FilePickerResult? result = await FilePicker.platform.pickFiles(
allowMultiple: true,
type: FileType.custom,
@ -53,14 +55,17 @@ Future importArchivesFromFile(
for (var file in result.files.reversed.toList()) {
(String, LocalExtensionType, Uint8List, String)? data =
itemType == ItemType.manga
? await ref.watch(getArchivesDataFromFileProvider(file.path!).future)
? await ref.watch(
getArchivesDataFromFileProvider(file.path!).future,
)
: null;
String name = _getName(file.path!);
if (init) {
manga.customCoverImage = itemType == ItemType.manga ? data!.$3 : null;
if (itemType == ItemType.manga) {
manga.customCoverImage = data!.$3.getCoverImage;
}
}
await isar.writeTxn(() async {
final mangaId = await isar.mangas.put(manga);
final List<Chapter> chapters = [];
@ -72,7 +77,12 @@ Future importArchivesFromFile(
book.Content!.Images!.containsKey("media/file0.png")
? book.Content!.Images!["media/file0.png"]!.Content
: book.Content!.Images!.values.first.Content;
await isar.mangas.put(manga..customCoverImage = coverImage);
await isar.mangas.put(
manga
..customCoverImage = coverImage == null
? null
: Uint8List.fromList(coverImage).getCoverImage,
);
}
for (var chapter in book.Chapters ?? []) {
chapters.add(
@ -103,7 +113,12 @@ Future importArchivesFromFile(
});
}
}
keepAlile.close();
return "";
} catch (e) {
keepAlile.close();
rethrow;
}
}
String _getName(String path) {
@ -117,3 +132,13 @@ String _getName(String path) {
'',
);
}
extension Uint8ListExtensions on Uint8List {
Uint8List? get getCoverImage {
final length = lengthInBytes / (1024 * 1024);
if (length < 5) {
return this;
}
return null;
}
}

View file

@ -65,7 +65,7 @@ final class ImportArchivesFromFileProvider
}
String _$importArchivesFromFileHash() =>
r'784b9d45958695faffdf04ee7c105c9b486122de';
r'a3fbf9d9ba7eacfa52366bfb10ba9f5f9117585b';
final class ImportArchivesFromFileFamily extends $Family
with

View file

@ -6,10 +6,20 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:path/path.dart' as p;
part 'archive_reader_providers.g.dart';
// Constants for supported file types
const List<String> _kImageExtensions = [
'.png',
'.jpg',
'.jpeg',
'.gif',
'.webp',
];
const List<String> _kArchiveExtensions = ['.cbz', '.zip', '.cbt', '.tar'];
@riverpod
Future<List<(String, LocalExtensionType, Uint8List, String)>>
getArchivesDataFromDirectory(Ref ref, String path) async {
return compute(_extractOnly, path);
return compute(_extractArchiveMetadataFromDirectory, path);
}
@riverpod
@ -17,7 +27,7 @@ Future<List<LocalArchive>> getArchiveDataFromDirectory(
Ref ref,
String path,
) async {
return compute(_extract, path);
return compute(_extractArchivesFromDirectory, path);
}
@riverpod
@ -25,7 +35,7 @@ Future<(String, LocalExtensionType, Uint8List, String)> getArchivesDataFromFile(
Ref ref,
String path,
) async {
return compute(_extractArchiveOnly, path);
return compute(_extractArchiveMetadata, path);
}
@riverpod
@ -33,200 +43,315 @@ Future<LocalArchive> getArchiveDataFromFile(Ref ref, String path) async {
return compute(_extractArchive, path);
}
Future<List<LocalArchive>> _extract(String data) async {
return await _searchForArchive(Directory(data));
}
Future<List<(String, LocalExtensionType, Uint8List, String)>> _extractOnly(
String data,
/// Extract full archive data from all archives in a directory (recursive)
Future<List<LocalArchive>> _extractArchivesFromDirectory(
String directoryPath,
) async {
return await _searchForArchiveOnly(Directory(data));
}
final archives = <LocalArchive>[];
List<LocalArchive> _list = [];
List<(String, LocalExtensionType, Uint8List, String)> _listOnly = [];
Future<List<LocalArchive>> _searchForArchive(Directory dir) async {
List<FileSystemEntity> entities = dir.listSync();
for (FileSystemEntity entity in entities) {
if (entity is Directory) {
_searchForArchive(entity);
} else if (entity is File) {
String path = entity.path;
if (_isArchiveFile(path)) {
final dd = await compute(_extractArchive, path);
_list.add(dd);
try {
final dir = Directory(directoryPath);
if (!dir.existsSync()) {
return archives;
}
}
}
return _list;
}
Future<List<(String, LocalExtensionType, Uint8List, String)>>
_searchForArchiveOnly(Directory dir) async {
List<FileSystemEntity> entities = dir.listSync();
for (FileSystemEntity entity in entities) {
if (entity is Directory) {
_searchForArchive(entity);
} else if (entity is File) {
String path = entity.path;
if (_isArchiveFile(path)) {
final dd = await compute(_extractArchiveOnly, path);
_listOnly.add(dd);
await _scanDirectoryRecursive(
dir,
onArchiveFound: (path) async {
try {
final archive = await _extractArchive(path);
archives.add(archive);
} catch (e) {
debugPrint('Error extracting archive at $path: $e');
}
}
}
return _listOnly;
}
bool _isImageFile(String path) {
List<String> imageExtensions = ['.png', '.jpg', '.jpeg'];
String extension = path.toLowerCase();
for (String imageExtension in imageExtensions) {
if (extension.endsWith(imageExtension)) {
return true;
}
}
return false;
}
bool _isArchiveFile(String path) {
List<String> archiveExtensions = ['.cbz', '.zip', 'cbt', 'tar'];
String extension = path.toLowerCase();
for (String archiveExtension in archiveExtensions) {
if (extension.endsWith(archiveExtension)) {
return true;
}
}
return false;
}
LocalArchive _extractArchive(String path) {
// Folder of images?
if (Directory(path).existsSync()) {
final dir = Directory(path);
final pages =
dir.listSync().whereType<File>().where((f) => _isImageFile(f.path)).map(
(f) {
return LocalImage()
..image = f.readAsBytesSync()
..name = p.basename(f.path);
},
).toList()..sort((a, b) => a.name!.compareTo(b.name!));
);
} catch (e) {
debugPrint('Error scanning directory $directoryPath: $e');
}
final localArchive = LocalArchive()
return archives;
}
/// Extract only metadata (cover) from all archives in a directory (recursive)
Future<List<(String, LocalExtensionType, Uint8List, String)>>
_extractArchiveMetadataFromDirectory(String directoryPath) async {
final metadata = <(String, LocalExtensionType, Uint8List, String)>[];
try {
final dir = Directory(directoryPath);
if (!dir.existsSync()) {
return metadata;
}
await _scanDirectoryRecursive(
dir,
onArchiveFound: (path) async {
try {
final data = await _extractArchiveMetadata(path);
metadata.add(data);
} catch (e) {
debugPrint('Error extracting metadata at $path: $e');
}
},
);
} catch (e) {
debugPrint('Error scanning directory $directoryPath: $e');
}
return metadata;
}
/// Recursively scan directory for archive files
Future<void> _scanDirectoryRecursive(
Directory dir, {
required Future<void> Function(String path) onArchiveFound,
}) async {
try {
final entities = dir.listSync();
for (final entity in entities) {
if (entity is Directory) {
// Recursive scan
await _scanDirectoryRecursive(entity, onArchiveFound: onArchiveFound);
} else if (entity is File) {
if (_isArchiveFile(entity.path)) {
await onArchiveFound(entity.path);
}
}
}
} catch (e) {
debugPrint('Error scanning directory ${dir.path}: $e');
}
}
/// Check if a file is an image based on extension
bool _isImageFile(String path) {
final extension = p.extension(path).toLowerCase();
return _kImageExtensions.contains(extension);
}
/// Check if a file is a supported archive based on extension
bool _isArchiveFile(String path) {
final extension = p.extension(path).toLowerCase();
return _kArchiveExtensions.any((ext) => extension.endsWith(ext));
}
/// Extract full archive with all images
Future<LocalArchive> _extractArchive(String path) async {
try {
// Handle directory of images
if (Directory(path).existsSync()) {
return await _extractFromImageFolder(path);
}
// Handle archive file
return _extractFromArchiveFile(path);
} catch (e) {
debugPrint('Error extracting archive from $path: $e');
rethrow;
}
}
/// Extract images from a folder
Future<LocalArchive> _extractFromImageFolder(String path) async {
final dir = Directory(path);
final imageFiles =
await dir
.list()
.where((entity) => entity is File && _isImageFile(entity.path))
.cast<File>()
.toList()
..sort((a, b) => a.path.compareTo(b.path));
if (imageFiles.isEmpty) {
throw Exception('No images found in folder: $path');
}
final images = imageFiles.map((file) {
return LocalImage()
..image = file.readAsBytesSync()
..name = p.basename(file.path);
}).toList();
return LocalArchive()
..path = path
..extensionType = LocalExtensionType.folder
..name = p.basename(path)
..images = pages
..coverImage = pages.first.image;
..images = images
..coverImage = images.first.image;
}
return localArchive;
}
/// Extract images from an archive file
LocalArchive _extractFromArchiveFile(String path) {
final extensionType = _getArchiveType(path);
final localArchive = LocalArchive()
..path = path
..extensionType = setTypeExtension(p.extension(path).replaceFirst(".", ""))
..name = p.basenameWithoutExtension(path);
Archive? archive;
final inputStream = InputFileStream(path);
final extensionType = localArchive.extensionType;
if (extensionType == LocalExtensionType.cbt ||
extensionType == LocalExtensionType.tar) {
archive = TarDecoder().decodeStream(inputStream);
} else {
archive = ZipDecoder().decodeStream(inputStream);
..extensionType = extensionType
..name = p.basenameWithoutExtension(path)
..images = [];
InputFileStream? inputStream;
try {
inputStream = InputFileStream(path);
final archive = _decodeArchive(inputStream, extensionType);
final imageFiles =
archive.files
.where(
(file) =>
file.isFile &&
_isImageFile(file.name) &&
!file.name.startsWith('.'),
)
.toList()
..sort((a, b) => a.name.compareTo(b.name));
if (imageFiles.isEmpty) {
throw Exception('No images found in archive: $path');
}
for (final file in archive.files) {
// Extract images
for (final file in imageFiles) {
final filename = file.name;
if (file.isFile) {
if (_isImageFile(filename) && !filename.startsWith('.')) {
final data = file.content;
if (filename.contains("cover")) {
if (filename.toLowerCase().contains('cover')) {
localArchive.coverImage = data;
} else {
}
localArchive.images!.add(
LocalImage()
..image = data
..name = p.basename(filename),
);
}
}
}
}
localArchive.images!.sort((a, b) => a.name!.compareTo(b.name!));
// Set cover image if not explicitly found
localArchive.coverImage ??= localArchive.images!.first.image;
return localArchive;
} finally {
inputStream?.close();
}
}
(String, LocalExtensionType, Uint8List, String) _extractArchiveOnly(
/// Extract only metadata (name, type, cover) from archive
Future<(String, LocalExtensionType, Uint8List, String)> _extractArchiveMetadata(
String path,
) {
// If it's a directory, just read its images:
if (Directory(path).existsSync()) {
) async {
try {
// Handle directory of images
if (await Directory(path).exists()) {
return await _extractMetadataFromImageFolder(path);
}
// Handle archive file
return _extractMetadataFromArchiveFile(path);
} catch (e) {
debugPrint('Error extracting metadata from $path: $e');
rethrow;
}
}
/// Extract metadata from image folder
Future<(String, LocalExtensionType, Uint8List, String)>
_extractMetadataFromImageFolder(String path) async {
final dir = Directory(path);
final images =
dir
.listSync()
.whereType<File>()
.where((f) => _isImageFile(f.path))
await dir
.list()
.where((entity) => entity is File && _isImageFile(entity.path))
.cast<File>()
.toList()
..sort((a, b) => a.path.compareTo(b.path));
if (images.isEmpty) {
throw Exception('No images found in folder: $path');
}
final cover = images.first.readAsBytesSync();
return (p.basename(path), LocalExtensionType.folder, cover, path);
}
final extensionType = setTypeExtension(
p.extension(path).replaceFirst('.', ''),
);
}
/// Extract metadata from archive file
(String, LocalExtensionType, Uint8List, String) _extractMetadataFromArchiveFile(
String path,
) {
final extensionType = _getArchiveType(path);
final name = p.basenameWithoutExtension(path);
Uint8List? coverImage;
Archive? archive;
final inputStream = InputFileStream(path);
InputFileStream? inputStream;
if (extensionType == LocalExtensionType.cbt ||
extensionType == LocalExtensionType.tar) {
archive = TarDecoder().decodeStream(inputStream);
} else {
archive = ZipDecoder().decodeStream(inputStream);
}
try {
inputStream = InputFileStream(path);
final archive = _decodeArchive(inputStream, extensionType);
final cover = archive.files.where(
// Look for cover image first
final coverFile = archive.files.firstWhere(
(file) =>
file.isFile && _isImageFile(file.name) && file.name.contains("cover"),
);
if (cover.isNotEmpty) {
coverImage = cover.first.content;
} else {
List<ArchiveFile> lArchive = archive.files
file.isFile &&
_isImageFile(file.name) &&
file.name.toLowerCase().contains('cover') &&
!file.name.startsWith('.'),
orElse: () {
// If no cover, get first image alphabetically
final imageFiles =
archive.files
.where(
(file) =>
file.isFile &&
_isImageFile(file.name) &&
!file.name.contains("cover"),
!file.name.startsWith('.'),
)
.toList();
lArchive.sort((a, b) => a.name.compareTo(b.name));
coverImage = lArchive.first.content;
.toList()
..sort((a, b) => a.name.compareTo(b.name));
if (imageFiles.isEmpty) {
throw Exception('No images found in archive: $path');
}
return imageFiles.first;
},
);
final coverImage = coverFile.content;
return (name, extensionType, coverImage, path);
} finally {
inputStream?.close();
}
}
/// Decode archive based on type
Archive _decodeArchive(InputFileStream stream, LocalExtensionType type) {
switch (type) {
case LocalExtensionType.cbt:
case LocalExtensionType.tar:
return TarDecoder().decodeStream(stream);
case LocalExtensionType.zip:
case LocalExtensionType.cbz:
case LocalExtensionType.folder:
return ZipDecoder().decodeStream(stream);
}
}
/// Get archive type from file extension
LocalExtensionType _getArchiveType(String path) {
final extension = p.extension(path).toLowerCase().replaceFirst('.', '');
return setTypeExtension(extension);
}
String getTypeExtension(LocalExtensionType type) {
return switch (type) {
LocalExtensionType.cbt => type.name,
LocalExtensionType.zip => type.name,
LocalExtensionType.tar => type.name,
_ => type.name,
};
return type.name;
}
LocalExtensionType setTypeExtension(String extension) {
return switch (extension) {
"cbt" => LocalExtensionType.cbt,
"zip" => LocalExtensionType.zip,
"tar" => LocalExtensionType.tar,
return switch (extension.toLowerCase()) {
'cbt' => LocalExtensionType.cbt,
'zip' => LocalExtensionType.zip,
'tar' => LocalExtensionType.tar,
'cbz' => LocalExtensionType.cbz,
_ => LocalExtensionType.cbz,
};
}

View file

@ -70,7 +70,7 @@ final class GetArchivesDataFromDirectoryProvider
}
String _$getArchivesDataFromDirectoryHash() =>
r'2a4d1a11e2b028e569ffd8a2700e4a1779bb9264';
r'2f343dfe03bb479e80e6343f389fce8830998f0e';
final class GetArchivesDataFromDirectoryFamily extends $Family
with
@ -154,7 +154,7 @@ final class GetArchiveDataFromDirectoryProvider
}
String _$getArchiveDataFromDirectoryHash() =>
r'49aa47895feafd9fa0c4f20e25d7674a3d54b212';
r'81705a8d04d4f4d1454a82b35e55eb2e0397ea6f';
final class GetArchiveDataFromDirectoryFamily extends $Family
with $FunctionalFamilyOverride<FutureOr<List<LocalArchive>>, String> {
@ -232,7 +232,7 @@ final class GetArchivesDataFromFileProvider
}
String _$getArchivesDataFromFileHash() =>
r'79874b548614b4410c19bca5f74978ec761742c5';
r'04d8ce722c077a7def61dda20ff18b23090fb646';
final class GetArchivesDataFromFileFamily extends $Family
with

View file

@ -14,6 +14,7 @@ import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/anime/widgets/desktop.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/modules/manga/reader/providers/crop_borders_provider.dart';
import 'package:mangayomi/modules/manga/reader/u_chap_data_preload.dart';
import 'package:mangayomi/modules/manga/reader/widgets/btn_chapter_list_dialog.dart';
@ -389,7 +390,7 @@ class _MangaChapterPageGalleryState
isar.mangas.putSync(
manga
..customCoverImage =
imageBytes
imageBytes.getCoverImage
..updatedAt = DateTime.now()
.millisecondsSinceEpoch,
);

View file

@ -176,9 +176,8 @@ class _CreateBackupState extends ConsumerState<CreateBackup> {
result = await FilePicker.platform
.getDirectoryPath();
}
if (result != null && context.mounted) {
ref.watch(
ref.read(
doBackUpProvider(
list: indexList,
path: result,