feature: add local archive & manage local chapter

This commit is contained in:
kodjomoustapha 2023-06-14 21:28:57 +01:00
parent 01c095d7e0
commit ac5995cb79
13 changed files with 839 additions and 374 deletions

View file

@ -38,7 +38,7 @@ class Manga {
bool? isLocalArchive;
String? customCoverImage;
List<byte>? customCoverImage;
@Backlink(to: "manga")
final chapters = IsarLinks<Chapter>();

View file

@ -30,7 +30,7 @@ const MangaSchema = CollectionSchema(
r'customCoverImage': PropertySchema(
id: 2,
name: r'customCoverImage',
type: IsarType.string,
type: IsarType.byteList,
),
r'dateAdded': PropertySchema(
id: 3,
@ -142,7 +142,7 @@ int _mangaEstimateSize(
{
final value = object.customCoverImage;
if (value != null) {
bytesCount += 3 + value.length * 3;
bytesCount += 3 + value.length;
}
}
{
@ -204,7 +204,7 @@ void _mangaSerialize(
) {
writer.writeString(offsets[0], object.author);
writer.writeLongList(offsets[1], object.categories);
writer.writeString(offsets[2], object.customCoverImage);
writer.writeByteList(offsets[2], object.customCoverImage);
writer.writeLong(offsets[3], object.dateAdded);
writer.writeString(offsets[4], object.description);
writer.writeBool(offsets[5], object.favorite);
@ -229,7 +229,7 @@ Manga _mangaDeserialize(
final object = Manga(
author: reader.readStringOrNull(offsets[0]),
categories: reader.readLongList(offsets[1]),
customCoverImage: reader.readStringOrNull(offsets[2]),
customCoverImage: reader.readByteList(offsets[2]),
dateAdded: reader.readLongOrNull(offsets[3]),
description: reader.readStringOrNull(offsets[4]),
favorite: reader.readBoolOrNull(offsets[5]) ?? false,
@ -261,7 +261,7 @@ P _mangaDeserializeProp<P>(
case 1:
return (reader.readLongList(offset)) as P;
case 2:
return (reader.readStringOrNull(offset)) as P;
return (reader.readByteList(offset)) as P;
case 3:
return (reader.readLongOrNull(offset)) as P;
case 4:
@ -716,55 +716,50 @@ extension MangaQueryFilter on QueryBuilder<Manga, Manga, QFilterCondition> {
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageEqualTo(
String? value, {
bool caseSensitive = true,
}) {
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageElementEqualTo(int value) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'customCoverImage',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageGreaterThan(
String? value, {
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageElementGreaterThan(
int value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
include: include,
property: r'customCoverImage',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageLessThan(
String? value, {
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageElementLessThan(
int value, {
bool include = false,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.lessThan(
include: include,
property: r'customCoverImage',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageBetween(
String? lower,
String? upper, {
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageElementBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.between(
@ -773,77 +768,95 @@ extension MangaQueryFilter on QueryBuilder<Manga, Manga, QFilterCondition> {
includeLower: includeLower,
upper: upper,
includeUpper: includeUpper,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageStartsWith(
String value, {
bool caseSensitive = true,
}) {
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageLengthEqualTo(int length) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.startsWith(
property: r'customCoverImage',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageEndsWith(
String value, {
bool caseSensitive = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.endsWith(
property: r'customCoverImage',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageContains(
String value,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.contains(
property: r'customCoverImage',
value: value,
caseSensitive: caseSensitive,
));
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageMatches(
String pattern,
{bool caseSensitive = true}) {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.matches(
property: r'customCoverImage',
wildcard: pattern,
caseSensitive: caseSensitive,
));
return query.listLength(
r'customCoverImage',
length,
true,
length,
true,
);
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition> customCoverImageIsEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.equalTo(
property: r'customCoverImage',
value: '',
));
return query.listLength(
r'customCoverImage',
0,
true,
0,
true,
);
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageIsNotEmpty() {
return QueryBuilder.apply(this, (query) {
return query.addFilterCondition(FilterCondition.greaterThan(
property: r'customCoverImage',
value: '',
));
return query.listLength(
r'customCoverImage',
0,
false,
999999,
true,
);
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageLengthLessThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'customCoverImage',
0,
true,
length,
include,
);
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageLengthGreaterThan(
int length, {
bool include = false,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'customCoverImage',
length,
include,
999999,
true,
);
});
}
QueryBuilder<Manga, Manga, QAfterFilterCondition>
customCoverImageLengthBetween(
int lower,
int upper, {
bool includeLower = true,
bool includeUpper = true,
}) {
return QueryBuilder.apply(this, (query) {
return query.listLength(
r'customCoverImage',
lower,
includeLower,
upper,
includeUpper,
);
});
}
@ -2384,18 +2397,6 @@ extension MangaQuerySortBy on QueryBuilder<Manga, Manga, QSortBy> {
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortByCustomCoverImage() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'customCoverImage', Sort.asc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortByCustomCoverImageDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'customCoverImage', Sort.desc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> sortByDateAdded() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'dateAdded', Sort.asc);
@ -2554,18 +2555,6 @@ extension MangaQuerySortThenBy on QueryBuilder<Manga, Manga, QSortThenBy> {
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenByCustomCoverImage() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'customCoverImage', Sort.asc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenByCustomCoverImageDesc() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'customCoverImage', Sort.desc);
});
}
QueryBuilder<Manga, Manga, QAfterSortBy> thenByDateAdded() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'dateAdded', Sort.asc);
@ -2737,11 +2726,9 @@ extension MangaQueryWhereDistinct on QueryBuilder<Manga, Manga, QDistinct> {
});
}
QueryBuilder<Manga, Manga, QDistinct> distinctByCustomCoverImage(
{bool caseSensitive = true}) {
QueryBuilder<Manga, Manga, QDistinct> distinctByCustomCoverImage() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'customCoverImage',
caseSensitive: caseSensitive);
return query.addDistinctBy(r'customCoverImage');
});
}
@ -2849,7 +2836,7 @@ extension MangaQueryProperty on QueryBuilder<Manga, Manga, QQueryProperty> {
});
}
QueryBuilder<Manga, String?, QQueryOperations> customCoverImageProperty() {
QueryBuilder<Manga, List<int>?, QQueryOperations> customCoverImageProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'customCoverImage');
});

View file

@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:archive/archive_io.dart';
import 'package:flutter/foundation.dart';
@ -7,7 +6,7 @@ import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'archive_reader_providers.g.dart';
@riverpod
Future<List<(String, LocalExtensionType, String, String)>>
Future<List<(String, LocalExtensionType, Uint8List, String)>>
getArchivesDataFromDirectory(
GetArchivesDataFromDirectoryRef ref, String path) async {
return compute(_extractOnly, path);
@ -20,7 +19,7 @@ Future<List<LocalArchive>> getArchiveDataFromDirectory(
}
@riverpod
Future<(String, LocalExtensionType, String, String)> getArchivesDataFromFile(
Future<(String, LocalExtensionType, Uint8List, String)> getArchivesDataFromFile(
GetArchivesDataFromFileRef ref, String path) async {
return compute(_extractArchiveOnly, path);
}
@ -35,13 +34,13 @@ Future<List<LocalArchive>> _extract(String data) async {
return await _searchForArchive(Directory(data));
}
Future<List<(String, LocalExtensionType, String, String)>> _extractOnly(
Future<List<(String, LocalExtensionType, Uint8List, String)>> _extractOnly(
String data) async {
return await _searchForArchiveOnly(Directory(data));
}
List<LocalArchive> _list = [];
List<(String, LocalExtensionType, String, String)> _listOnly = [];
List<(String, LocalExtensionType, Uint8List, String)> _listOnly = [];
Future<List<LocalArchive>> _searchForArchive(Directory dir) async {
List<FileSystemEntity> entities = dir.listSync();
for (FileSystemEntity entity in entities) {
@ -58,7 +57,7 @@ Future<List<LocalArchive>> _searchForArchive(Directory dir) async {
return _list;
}
Future<List<(String, LocalExtensionType, String, String)>>
Future<List<(String, LocalExtensionType, Uint8List, String)>>
_searchForArchiveOnly(Directory dir) async {
List<FileSystemEntity> entities = dir.listSync();
for (FileSystemEntity entity in entities) {
@ -139,7 +138,7 @@ LocalArchive _extractArchive(String path) {
return localArchive;
}
(String, LocalExtensionType, String, String) _extractArchiveOnly(String path) {
(String, LocalExtensionType, Uint8List, String) _extractArchiveOnly(String path) {
final extensionType =
setTypeExtension(path.split('/').last.split("\\").last.split(".").last);
final name = path
@ -148,7 +147,7 @@ LocalArchive _extractArchive(String path) {
.split("\\")
.last
.replaceAll(RegExp(r'\.(cbz|zip|cbt|tar)'), '');
String? coverImage;
Uint8List? coverImage;
Archive? archive;
final inputStream = InputFileStream(path);
@ -163,16 +162,20 @@ LocalArchive _extractArchive(String path) {
final cover = archive.files.where((file) =>
file.isFile && _isImageFile(file.name) && file.name.contains("cover"));
final coverImg = cover.isNotEmpty
? cover.first.content as Uint8List
: archive.files
.where((file) =>
file.isFile &&
_isImageFile(file.name) &&
!file.name.contains("cover"))
.first
.content as Uint8List;
coverImage = base64.encode(coverImg);
if (cover.isNotEmpty) {
coverImage = cover.first.content as Uint8List;
} else {
List<ArchiveFile> lArchive = archive.files
.where((file) =>
file.isFile &&
_isImageFile(file.name) &&
!file.name.contains("cover"))
.toList();
lArchive.sort(
(a, b) => a.name.compareTo(b.name),
);
coverImage = lArchive.first.content as Uint8List;
}
return (name, extensionType, coverImage, path);
}

View file

@ -7,7 +7,7 @@ part of 'archive_reader_providers.dart';
// **************************************************************************
String _$getArchivesDataFromDirectoryHash() =>
r'92989ce549951f237423efa91747560507c7b2d0';
r'7ca5e7d4a2a79745c92dd0370703c614406be2ad';
/// Copied from Dart SDK
class _SystemHash {
@ -31,7 +31,7 @@ class _SystemHash {
}
typedef GetArchivesDataFromDirectoryRef = AutoDisposeFutureProviderRef<
List<(String, LocalExtensionType, String, String)>>;
List<(String, LocalExtensionType, Uint8List, String)>>;
/// See also [getArchivesDataFromDirectory].
@ProviderFor(getArchivesDataFromDirectory)
@ -40,7 +40,7 @@ const getArchivesDataFromDirectoryProvider =
/// See also [getArchivesDataFromDirectory].
class GetArchivesDataFromDirectoryFamily extends Family<
AsyncValue<List<(String, LocalExtensionType, String, String)>>> {
AsyncValue<List<(String, LocalExtensionType, Uint8List, String)>>> {
/// See also [getArchivesDataFromDirectory].
const GetArchivesDataFromDirectoryFamily();
@ -79,7 +79,7 @@ class GetArchivesDataFromDirectoryFamily extends Family<
/// See also [getArchivesDataFromDirectory].
class GetArchivesDataFromDirectoryProvider extends AutoDisposeFutureProvider<
List<(String, LocalExtensionType, String, String)>> {
List<(String, LocalExtensionType, Uint8List, String)>> {
/// See also [getArchivesDataFromDirectory].
GetArchivesDataFromDirectoryProvider(
this.path,
@ -202,17 +202,17 @@ class GetArchiveDataFromDirectoryProvider
}
String _$getArchivesDataFromFileHash() =>
r'b2f163e5deb0a4f344f6ce5e6aab0c226b644f3b';
r'f118f903a693c2f2ad5ec2452430a1eb10b661b2';
typedef GetArchivesDataFromFileRef = AutoDisposeFutureProviderRef<
(String, LocalExtensionType, String, String)>;
(String, LocalExtensionType, Uint8List, String)>;
/// See also [getArchivesDataFromFile].
@ProviderFor(getArchivesDataFromFile)
const getArchivesDataFromFileProvider = GetArchivesDataFromFileFamily();
/// See also [getArchivesDataFromFile].
class GetArchivesDataFromFileFamily
extends Family<AsyncValue<(String, LocalExtensionType, String, String)>> {
class GetArchivesDataFromFileFamily extends Family<
AsyncValue<(String, LocalExtensionType, Uint8List, String)>> {
/// See also [getArchivesDataFromFile].
const GetArchivesDataFromFileFamily();
@ -251,7 +251,7 @@ class GetArchivesDataFromFileFamily
/// See also [getArchivesDataFromFile].
class GetArchivesDataFromFileProvider extends AutoDisposeFutureProvider<
(String, LocalExtensionType, String, String)> {
(String, LocalExtensionType, Uint8List, String)> {
/// See also [getArchivesDataFromFile].
GetArchivesDataFromFileProvider(
this.path,

View file

@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
@ -134,7 +133,6 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
),
itemBuilder: (context, History element) {
final manga = element.chapter.value!.manga.value!;
bool isLocalArchive = manga.isLocalArchive ?? false;
final chapter = element.chapter.value!;
return ElevatedButton(
style: ElevatedButton.styleFrom(
@ -169,9 +167,9 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen> {
},
child: ClipRRect(
borderRadius: BorderRadius.circular(7),
child: isLocalArchive
? Image.memory(base64
.decode(manga.customCoverImage!))
child: manga.customCoverImage != null
? Image.memory(
manga.customCoverImage as Uint8List)
: cachedNetworkImage(
headers: ref.watch(headersProvider(
source: manga.source!)),

View file

@ -1,18 +1,14 @@
import 'dart:developer';
import 'package:draggable_menu/draggable_menu.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:isar/isar.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/category.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/models/settings.dart';
import 'package:mangayomi/modules/archive_reader/providers/archive_reader_providers.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/providers/storage_provider.dart';
import 'package:mangayomi/utils/colors.dart';
import 'package:mangayomi/utils/media_query.dart';
@ -1528,55 +1524,20 @@ _importArchiveBD(BuildContext context) {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
onPressed: () async {
FilePickerResult? result = await FilePicker.platform
.pickFiles(
type: FileType.custom,
allowedExtensions: [
'cbz',
'zip',
]);
if (result != null) {
for (var file in result.files) {
final data = await ref.watch(
getArchivesDataFromFileProvider(file.path!)
.future);
final manga = Manga(
favorite: true,
source: 'archive',
author: '',
genre: [],
imageUrl: '',
lang: '',
link: '',
name: data.$1,
dateAdded:
DateTime.now().millisecondsSinceEpoch,
lastUpdate:
DateTime.now().millisecondsSinceEpoch,
status: Status.unknown,
description: '',
isLocalArchive: true,
customCoverImage: data.$3);
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final chapters =
Chapter(name: data.$1, archivePath: data.$4)
..manga.value = manga;
isar.chapters.putSync(chapters);
chapters.manga.saveSync();
});
}
} else {}
await ref.watch(
importArchivesFromFileProvider(null).future);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Icon(Icons.archive_outlined),
const SizedBox(
height: 10,
),
Text(".cbz or .zip",
style: Theme.of(context).textTheme.bodySmall)
Text(".cbz or .zip files",
style: TextStyle(
color: Theme.of(context)
.textTheme
.bodySmall!
.color,
fontSize: 10))
],
),
),
@ -1590,51 +1551,23 @@ _importArchiveBD(BuildContext context) {
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10))),
onPressed: () async {
String? result =
await FilePicker.platform.getDirectoryPath();
if (result != null) {
final datas = await ref.watch(
getArchivesDataFromDirectoryProvider(result)
.future);
for (var data in datas) {
final manga = Manga(
favorite: true,
source: 'archive',
author: '',
genre: [],
imageUrl: '',
lang: '',
link: '',
name: data.$1,
dateAdded:
DateTime.now().millisecondsSinceEpoch,
lastUpdate:
DateTime.now().millisecondsSinceEpoch,
status: Status.unknown,
description: '',
isLocalArchive: true,
customCoverImage: data.$3);
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final chapters =
Chapter(name: data.$1, archivePath: data.$4)
..manga.value = manga;
isar.chapters.putSync(chapters);
chapters.manga.saveSync();
});
}
}
await ref.watch(
importArchivesFromDirectoryProvider.future);
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Icon(Icons.folder),
const SizedBox(
height: 10,
),
Text("From folder",
style: Theme.of(context).textTheme.bodySmall)
Text(
"From folder (.cbz or .zip files) ",
style: TextStyle(
color: Theme.of(context)
.textTheme
.bodySmall!
.color,
fontSize: 10),
textAlign: TextAlign.center,
)
],
),
),

View file

@ -0,0 +1,82 @@
import 'package:file_picker/file_picker.dart';
import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/modules/archive_reader/providers/archive_reader_providers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'local_archive.g.dart';
@riverpod
Future importArchivesFromDirectory(ImportArchivesFromDirectoryRef ref) async {
String? result = await FilePicker.platform.getDirectoryPath();
if (result != null) {
final datas =
await ref.watch(getArchivesDataFromDirectoryProvider(result).future);
for (var data in datas) {
final manga = Manga(
favorite: true,
source: 'archive',
author: '',
genre: [],
imageUrl: '',
lang: '',
link: '',
name: data.$1,
dateAdded: DateTime.now().millisecondsSinceEpoch,
lastUpdate: DateTime.now().millisecondsSinceEpoch,
status: Status.unknown,
description: '',
isLocalArchive: true,
customCoverImage: data.$3);
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final chapters = Chapter(name: data.$1, archivePath: data.$4)
..manga.value = manga;
isar.chapters.putSync(chapters);
chapters.manga.saveSync();
});
}
}
return "";
}
@riverpod
Future importArchivesFromFile(ImportArchivesFromFileRef ref,
Manga? mManga) async {
FilePickerResult? result = await FilePicker.platform
.pickFiles(type: FileType.custom, allowedExtensions: [
'cbz',
'zip',
]);
if (result != null) {
for (var file in result.files) {
final data =
await ref.watch(getArchivesDataFromFileProvider(file.path!).future);
final manga = mManga ??
Manga(
favorite: true,
source: 'archive',
author: '',
genre: [],
imageUrl: '',
lang: '',
link: '',
name: data.$1,
dateAdded: DateTime.now().millisecondsSinceEpoch,
lastUpdate: DateTime.now().millisecondsSinceEpoch,
status: Status.unknown,
description: '',
isLocalArchive: true,
customCoverImage: data.$3);
isar.writeTxnSync(() {
isar.mangas.putSync(manga);
final chapters = Chapter(name: data.$1, archivePath: data.$4)
..manga.value = manga;
isar.chapters.putSync(chapters);
chapters.manga.saveSync();
});
}
}
return "";
}

View file

@ -0,0 +1,131 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'local_archive.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$importArchivesFromDirectoryHash() =>
r'c42265b5ccec477da1a39964a4fffeaf37b49164';
/// See also [importArchivesFromDirectory].
@ProviderFor(importArchivesFromDirectory)
final importArchivesFromDirectoryProvider =
AutoDisposeFutureProvider<dynamic>.internal(
importArchivesFromDirectory,
name: r'importArchivesFromDirectoryProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$importArchivesFromDirectoryHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef ImportArchivesFromDirectoryRef = AutoDisposeFutureProviderRef<dynamic>;
String _$importArchivesFromFileHash() =>
r'bc892e1fb32c5d5ec639ad5f76c27996605181f5';
/// Copied from Dart SDK
class _SystemHash {
_SystemHash._();
static int combine(int hash, int value) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + value);
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
return hash ^ (hash >> 6);
}
static int finish(int hash) {
// ignore: parameter_assignments
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
// ignore: parameter_assignments
hash = hash ^ (hash >> 11);
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
}
}
typedef ImportArchivesFromFileRef = AutoDisposeFutureProviderRef<dynamic>;
/// See also [importArchivesFromFile].
@ProviderFor(importArchivesFromFile)
const importArchivesFromFileProvider = ImportArchivesFromFileFamily();
/// See also [importArchivesFromFile].
class ImportArchivesFromFileFamily extends Family<AsyncValue<dynamic>> {
/// See also [importArchivesFromFile].
const ImportArchivesFromFileFamily();
/// See also [importArchivesFromFile].
ImportArchivesFromFileProvider call(
Manga? mManga,
) {
return ImportArchivesFromFileProvider(
mManga,
);
}
@override
ImportArchivesFromFileProvider getProviderOverride(
covariant ImportArchivesFromFileProvider provider,
) {
return call(
provider.mManga,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'importArchivesFromFileProvider';
}
/// See also [importArchivesFromFile].
class ImportArchivesFromFileProvider
extends AutoDisposeFutureProvider<dynamic> {
/// See also [importArchivesFromFile].
ImportArchivesFromFileProvider(
this.mManga,
) : super.internal(
(ref) => importArchivesFromFile(
ref,
mManga,
),
from: importArchivesFromFileProvider,
name: r'importArchivesFromFileProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$importArchivesFromFileHash,
dependencies: ImportArchivesFromFileFamily._dependencies,
allTransitiveDependencies:
ImportArchivesFromFileFamily._allTransitiveDependencies,
);
final Manga? mManga;
@override
bool operator ==(Object other) {
return other is ImportArchivesFromFileProvider && other.mManga == mManga;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, mManga.hashCode);
return _SystemHash.finish(hash);
}
}
// ignore_for_file: unnecessary_raw_strings, subtype_of_sealed_class, invalid_use_of_internal_member, do_not_use_environment, prefer_const_constructors, public_member_api_docs, avoid_private_typedef_functions

View file

@ -1,4 +1,4 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -56,9 +56,9 @@ class LibraryGridViewWidget extends StatelessWidget {
isComfortableGrid: isComfortableGrid,
),
isComfortableGrid: isComfortableGrid,
image: isLocalArchive
image: entriesManga[index].customCoverImage != null
? MemoryImage(
base64.decode(entriesManga[index].customCoverImage!))
entriesManga[index].customCoverImage as Uint8List)
as ImageProvider
: CachedNetworkImageProvider(
entriesManga[index].imageUrl!,

View file

@ -1,5 +1,4 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -97,11 +96,12 @@ class LibraryListViewWidget extends StatelessWidget {
fit: BoxFit.cover,
width: 40,
height: 45,
image: isLocalArchive
? MemoryImage(base64.decode(
entriesManga[index]
.customCoverImage!))
as ImageProvider
image: entriesManga[index]
.customCoverImage !=
null
? MemoryImage(
entriesManga[index].customCoverImage
as Uint8List) as ImageProvider
: CachedNetworkImageProvider(
entriesManga[index].imageUrl!,
headers: ref.watch(headersProvider(

View file

@ -1,8 +1,9 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:cached_network_image/cached_network_image.dart';
import 'package:draggable_menu/draggable_menu.dart';
import 'package:draggable_scrollbar/draggable_scrollbar.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -12,6 +13,7 @@ import 'package:mangayomi/main.dart';
import 'package:mangayomi/models/chapter.dart';
import 'package:mangayomi/models/download.dart';
import 'package:mangayomi/models/manga.dart';
import 'package:mangayomi/modules/library/providers/local_archive.dart';
import 'package:mangayomi/sources/utils/utils.dart';
import 'package:mangayomi/utils/cached_network.dart';
import 'package:mangayomi/utils/colors.dart';
@ -65,7 +67,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
final offetProvider = StateProvider((ref) => 0.0);
bool _expanded = false;
ScrollController _scrollController = ScrollController();
late final isLocalArchive = widget.manga!.isLocalArchive ?? false;
@override
Widget build(BuildContext context) {
final isLongPressed = ref.watch(isLongPressedStateProvider);
@ -192,9 +194,9 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
child: ref.watch(offetProvider) == 0.0
? Stack(
children: [
widget.manga!.isLocalArchive ?? false
widget.manga!.customCoverImage != null
? Image.memory(
base64Decode(widget.manga!.customCoverImage!),
widget.manga!.customCoverImage as Uint8List,
width: mediaWidth(context, 1),
height: 300,
fit: BoxFit.cover)
@ -327,12 +329,13 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
? Colors.transparent
: Theme.of(context).scaffoldBackgroundColor,
actions: [
IconButton(
splashRadius: 20,
onPressed: () {},
icon: const Icon(
Icons.download_outlined,
)),
if (!isLocalArchive)
IconButton(
splashRadius: 20,
onPressed: () {},
icon: const Icon(
Icons.download_outlined,
)),
IconButton(
splashRadius: 20,
onPressed: () {
@ -349,11 +352,13 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
const PopupMenuItem<int>(
value: 0,
child: Text("Edit categories")),
if (widget.manga!.favorite)
if (!isLocalArchive)
if (widget.manga!.favorite)
const PopupMenuItem<int>(
value: 1, child: Text("Migrate")),
if (!isLocalArchive)
const PopupMenuItem<int>(
value: 1, child: Text("Migrate")),
const PopupMenuItem<int>(
value: 2, child: Text("Share")),
value: 2, child: Text("Share")),
];
}, onSelected: (value) {
if (value == 0) {
@ -413,12 +418,13 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
return isTablet(context)
? Column(
children: [
//Description
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.start,
mainAxisAlignment: isLocalArchive
? MainAxisAlignment
.spaceBetween
: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets
@ -429,7 +435,36 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
fontWeight:
FontWeight.bold),
),
)
),
if (isLocalArchive)
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding:
const EdgeInsets
.all(5),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius
.circular(
5))),
icon: Icon(Icons.add,
color: secondaryColor(
context)),
label: Text(
'Add chapters',
style: TextStyle(
fontWeight:
FontWeight.bold,
color: secondaryColor(
context)),
),
onPressed: () async {
await ref.watch(
importArchivesFromFileProvider(
widget.manga)
.future);
},
)
],
),
),
@ -597,46 +632,120 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
)),
),
),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
isar.txnSync(() {
for (var chapter
in ref.watch(chaptersListStateProvider)) {
final entries = isar.downloads
.filter()
.idIsNotNull()
.chapterIdEqualTo(chapter.id)
.findAllSync();
if (entries.isEmpty ||
!entries.first.isDownload!) {
ref.watch(downloadChapterProvider(
chapter: chapter));
if (!isLocalArchive)
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
isar.txnSync(() {
for (var chapter
in ref.watch(chaptersListStateProvider)) {
final entries = isar.downloads
.filter()
.idIsNotNull()
.chapterIdEqualTo(chapter.id)
.findAllSync();
if (entries.isEmpty ||
!entries.first.isDownload!) {
ref.watch(downloadChapterProvider(
chapter: chapter));
}
}
}
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Icon(
Icons.download_outlined,
color:
Theme.of(context).textTheme.bodyLarge!.color!,
)),
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Icon(
Icons.download_outlined,
color: Theme.of(context)
.textTheme
.bodyLarge!
.color!,
)),
),
),
)
if (isLocalArchive)
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text(
"Delete chapters ?",
),
actions: [
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Cancel")),
const SizedBox(
width: 15,
),
TextButton(
onPressed: () async {
isar.writeTxnSync(() {
for (var chapter in ref.watch(
chaptersListStateProvider)) {
isar.chapters
.deleteSync(
chapter.id!);
}
});
ref
.read(
isLongPressedStateProvider
.notifier)
.update(false);
ref
.read(
chaptersListStateProvider
.notifier)
.clear();
if (mounted) {
Navigator.pop(context);
}
},
child: const Text("Delete")),
],
)
],
);
});
},
child: Icon(
Icons.delete_outline_outlined,
color: Theme.of(context)
.textTheme
.bodyLarge!
.color!,
)),
),
)
],
),
);
@ -686,20 +795,21 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
Consumer(builder: (context, ref, chil) {
return Column(
children: [
ListTileChapterFilter(
label: "Downloaded",
type: ref.watch(
chapterFilterDownloadedStateProvider(
mangaId: widget.manga!.id!)),
onTap: () {
ref
.read(
chapterFilterDownloadedStateProvider(
mangaId:
widget.manga!.id!)
.notifier)
.update();
}),
if (!isLocalArchive)
ListTileChapterFilter(
label: "Downloaded",
type: ref.watch(
chapterFilterDownloadedStateProvider(
mangaId: widget.manga!.id!)),
onTap: () {
ref
.read(
chapterFilterDownloadedStateProvider(
mangaId: widget
.manga!.id!)
.notifier)
.update();
}),
ListTileChapterFilter(
label: "Unread",
type: ref.watch(
@ -932,20 +1042,31 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
),
Column(
children: [
SizedBox(
height: 180,
width: mediaWidth(context, 1),
child: Row(
children: [
_coverCard(),
Expanded(child: _titles()),
],
),
Stack(
children: [
SizedBox(
height: 180,
width: mediaWidth(context, 1),
child: Row(
children: [
_coverCard(),
Expanded(child: _titles()),
],
),
),
if (isLocalArchive)
Positioned(
top: 0,
right: 0,
child: IconButton(
onPressed: () {
_editLocaleArchiveInfos();
},
icon: const CircleAvatar(
child: Icon(Icons.edit_outlined))))
],
),
if (widget.manga!.isLocalArchive != null
? !widget.manga!.isLocalArchive!
: false)
_actionFavouriteAndWebview(),
if (!isLocalArchive) _actionFavouriteAndWebview(),
Container(
color: Theme.of(context).scaffoldBackgroundColor,
child: Column(
@ -1047,7 +1168,9 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisAlignment: isLocalArchive
? MainAxisAlignment.spaceBetween
: MainAxisAlignment.start,
children: [
Padding(
padding:
@ -1057,7 +1180,29 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
style: const TextStyle(
fontWeight: FontWeight.bold),
),
)
),
if (isLocalArchive)
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(5),
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.circular(5))),
icon: Icon(Icons.add,
color: secondaryColor(context)),
label: Text(
'Add chapters',
style: TextStyle(
fontWeight: FontWeight.bold,
color: secondaryColor(context)),
),
onPressed: () async {
await ref.watch(
importArchivesFromFileProvider(
widget.manga)
.future);
},
)
],
),
),
@ -1073,11 +1218,15 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
}
Widget _coverCard() {
final imageProvider = widget.manga!.customCoverImage != null
? MemoryImage(widget.manga!.customCoverImage as Uint8List)
as ImageProvider
: CachedNetworkImageProvider(widget.manga!.imageUrl!);
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 13),
child: GestureDetector(
onTap: () {
_openImage(widget.manga!.imageUrl!);
_openImage(imageProvider);
},
child: SizedBox(
width: 65 * 1.5,
@ -1086,10 +1235,7 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(5)),
image: DecorationImage(
image: widget.manga!.isLocalArchive ?? false
? MemoryImage(base64Decode(widget.manga!.customCoverImage!))
as ImageProvider
: CachedNetworkImageProvider(widget.manga!.imageUrl!),
image: imageProvider,
fit: BoxFit.cover,
),
),
@ -1165,32 +1311,219 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
);
}
_openImage(String url) {
_openImage(ImageProvider imageProvider) {
showDialog(
context: context,
builder: (context) {
return Scaffold(
backgroundColor: Colors.transparent,
appBar: AppBar(
backgroundColor: Colors.transparent,
),
body: GestureDetector(
onTap: () => Navigator.pop(context),
child: PhotoViewGallery.builder(
itemCount: 1,
builder: (context, index) {
return PhotoViewGalleryPageOptions(
imageProvider: CachedNetworkImageProvider(url),
minScale: PhotoViewComputedScale.contained,
maxScale: PhotoViewComputedScale.covered,
);
},
loadingBuilder: (context, event) {
return const ProgressCenter();
},
),
body: Stack(
children: [
GestureDetector(
onTap: () => Navigator.pop(context),
child: PhotoViewGallery.builder(
backgroundDecoration:
const BoxDecoration(color: Colors.transparent),
itemCount: 1,
builder: (context, index) {
return PhotoViewGalleryPageOptions(
imageProvider: imageProvider,
minScale: PhotoViewComputedScale.contained,
maxScale: 2.0,
);
},
loadingBuilder: (context, event) {
return const ProgressCenter();
},
),
),
Positioned(
bottom: 0,
right: 0,
child: Row(
children: [
if (!isLocalArchive)
if (widget.manga!.customCoverImage != null)
PopupMenuButton(
itemBuilder: (context) {
return [
const PopupMenuItem<int>(
value: 0, child: Text("Delete")),
const PopupMenuItem<int>(
value: 1, child: Text("Edit")),
];
},
onSelected: (value) async {
final manga = widget.manga!;
if (value == 0) {
isar.writeTxnSync(() {
isar.mangas
.putSync(manga..customCoverImage = null);
});
} else if (value == 1) {
FilePickerResult? result =
await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: [
'png',
'jpg',
'jpeg'
]);
if (result != null) {
if (result.files.first.size < 5000000) {
final customCoverImage =
File(result.files.first.path!)
.readAsBytesSync();
isar.writeTxnSync(() {
isar.mangas.putSync(manga
..customCoverImage = customCoverImage);
});
}
}
}
if (mounted) {
Navigator.pop(context);
}
},
child: const Padding(
padding: EdgeInsets.all(8.0),
child: CircleAvatar(
child: Icon(Icons.edit_outlined)),
),
),
// IconButton(
// onPressed: () async {
// Uint8List? bytes;
// if (isLocalArchive) {
// bytes =
// widget.manga!.customCoverImage as Uint8List?;
// }
// await Share.shareXFiles([
// XFile.fromData(bytes!,
// name: widget.manga!.name,
// mimeType: 'image/jpeg')
// ]);
// },
// icon: const CircleAvatar(child: Icon(Icons.share))),
if (isLocalArchive ||
widget.manga!.customCoverImage == null)
IconButton(
onPressed: () async {
FilePickerResult? result =
await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: [
'png',
'jpg',
'jpeg'
]);
if (result != null) {
if (result.files.first.size < 5000000) {
final manga = widget.manga!;
final customCoverImage =
File(result.files.first.path!)
.readAsBytesSync();
isar.writeTxnSync(() {
isar.mangas.putSync(manga
..customCoverImage = customCoverImage);
});
if (mounted) {
Navigator.pop(context);
}
}
}
},
icon: const CircleAvatar(
child: Icon(Icons.edit_outlined))),
],
),
)
],
),
);
});
}
_editLocaleArchiveInfos() {
TextEditingController? name =
TextEditingController(text: widget.manga!.name!);
TextEditingController? description =
TextEditingController(text: widget.manga!.description!);
// TextEditingController? tag;
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: const Text(
"Edit",
),
content: SizedBox(
height: 200,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Padding(
padding: const EdgeInsets.only(top: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 15),
child: Text("Name"),
),
TextFormField(
controller: name,
),
],
),
),
Padding(
padding: const EdgeInsets.only(top: 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Padding(
padding: EdgeInsets.only(left: 15),
child: Text("Description"),
),
TextFormField(
controller: description,
),
],
),
)
],
),
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: const Text("Cancel")),
const SizedBox(
width: 15,
),
TextButton(
onPressed: () {
isar.writeTxnSync(() {
final manga = widget.manga!;
manga.description = description.text;
manga.name = name.text;
isar.mangas.putSync(manga);
});
Navigator.pop(context);
},
child: const Text("Edit")),
],
)
],
);
});
}
}

View file

@ -14,7 +14,7 @@ class ReadMoreWidget extends StatefulWidget {
class ReadMoreWidgetState extends State<ReadMoreWidget>
with TickerProviderStateMixin {
bool expanded = false;
late bool expanded = widget.text.trim().length < 232;
@override
Widget build(BuildContext context) {
return Column(
@ -33,11 +33,7 @@ class ReadMoreWidgetState extends State<ReadMoreWidget>
widget.text.trim(),
expandText: '',
maxLines: 3,
expanded: false,
onPrefixTap: () {
setState(() => expanded = !expanded);
widget.onChanged(expanded);
},
expanded: expanded,
linkColor: Theme.of(context).scaffoldBackgroundColor,
animation: true,
collapseOnTextTap: true,

View file

@ -502,20 +502,22 @@ class _MangaChapterPageGalleryState
icon: Icon(_isBookmarked
? Icons.bookmark
: Icons.bookmark_border_outlined)),
IconButton(
onPressed: () {
final manga = widget.chapter.manga.value!;
String url = getMangaAPIUrl(manga.source!).isEmpty
? manga.link!
: "${getMangaBaseUrl(manga.source!)}${manga.link!}";
Map<String, String> data = {
'url': url,
'source': manga.source!,
'title': widget.chapter.name!
};
context.push("/mangawebview", extra: data);
},
icon: const Icon(Icons.public)),
if ((widget.chapter.manga.value!.isLocalArchive ?? false) ==
false)
IconButton(
onPressed: () {
final manga = widget.chapter.manga.value!;
String url = getMangaAPIUrl(manga.source!).isEmpty
? manga.link!
: "${getMangaBaseUrl(manga.source!)}${manga.link!}";
Map<String, String> data = {
'url': url,
'source': manga.source!,
'title': widget.chapter.name!
};
context.push("/mangawebview", extra: data);
},
icon: const Icon(Icons.public)),
],
backgroundColor: _backgroundColor(context),
),
@ -578,7 +580,7 @@ class _MangaChapterPageGalleryState
child: Transform.scale(
scaleX: !_isReversHorizontal ? 1 : -1,
child: SizedBox(
width: 25,
width: 30,
child: Consumer(
builder: (context, ref, child) {
final currentIndex = ref.watch(
@ -628,7 +630,7 @@ class _MangaChapterPageGalleryState
child: Transform.scale(
scaleX: !_isReversHorizontal ? 1 : -1,
child: SizedBox(
width: 25,
width: 30,
child: Text(
"${widget.readerController.getPageLength(widget.url)}",
style: const TextStyle(
@ -730,7 +732,7 @@ class _MangaChapterPageGalleryState
final cropBorders = ref.watch(cropBordersStateProvider);
return IconButton(
onPressed: () {
_cropImage();
// _cropImage();
ref
.read(cropBordersStateProvider.notifier)
.set(!cropBorders);