mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-20 19:12:04 +00:00
feature: add local archive & manage local chapter
This commit is contained in:
parent
01c095d7e0
commit
ac5995cb79
13 changed files with 839 additions and 374 deletions
|
|
@ -38,7 +38,7 @@ class Manga {
|
|||
|
||||
bool? isLocalArchive;
|
||||
|
||||
String? customCoverImage;
|
||||
List<byte>? customCoverImage;
|
||||
|
||||
@Backlink(to: "manga")
|
||||
final chapters = IsarLinks<Chapter>();
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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!)),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
|||
82
lib/modules/library/providers/local_archive.dart
Normal file
82
lib/modules/library/providers/local_archive.dart
Normal 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 "";
|
||||
}
|
||||
131
lib/modules/library/providers/local_archive.g.dart
Normal file
131
lib/modules/library/providers/local_archive.g.dart
Normal 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
|
||||
|
|
@ -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!,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue