mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-21 16:01:58 +00:00
Refactor
This commit is contained in:
parent
1c9a915c30
commit
1256e608c7
26 changed files with 2206 additions and 2054 deletions
|
|
@ -2014,7 +2014,8 @@ mp.register_script_message('call_button_${button.id}_long', button${button.id}lo
|
||||||
}
|
}
|
||||||
|
|
||||||
void _resize(BoxFit fit) async {
|
void _resize(BoxFit fit) async {
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
// Wait for the widget tree to settle before updating fit
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
_key.currentState?.update(
|
_key.currentState?.update(
|
||||||
fit: fit,
|
fit: fit,
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,8 @@ class _SubtitlesWidgetSearchState extends ConsumerState<SubtitlesWidgetSearch> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
await Future.delayed(const Duration(microseconds: 100));
|
// Yield to microtask queue so initState completes before async work
|
||||||
|
await Future(() {});
|
||||||
try {
|
try {
|
||||||
titles = await fetchImdbTitles(query);
|
titles = await fetchImdbTitles(query);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,8 @@ class _CodeEditorPageState extends ConsumerState<CodeEditorPage> {
|
||||||
_logSubscription = _logStreamController.stream.listen((event) async {
|
_logSubscription = _logStreamController.stream.listen((event) async {
|
||||||
_addLog(event);
|
_addLog(event);
|
||||||
try {
|
try {
|
||||||
await Future.delayed(const Duration(milliseconds: 5));
|
// Wait for the frame to complete so maxScrollExtent is updated
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
if (_scrollController.hasClients) {
|
if (_scrollController.hasClients) {
|
||||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -76,7 +76,8 @@ class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_query = "";
|
_query = "";
|
||||||
});
|
});
|
||||||
await Future.delayed(const Duration(milliseconds: 10));
|
// Yield a frame so the empty state is rendered before re-querying
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
setState(() {
|
setState(() {
|
||||||
_query = value;
|
_query = value;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
File diff suppressed because it is too large
Load diff
161
lib/modules/library/providers/library_filter_provider.dart
Normal file
161
lib/modules/library/providers/library_filter_provider.dart
Normal file
|
|
@ -0,0 +1,161 @@
|
||||||
|
import 'package:isar_community/isar.dart';
|
||||||
|
import 'package:mangayomi/main.dart';
|
||||||
|
import 'package:mangayomi/models/download.dart';
|
||||||
|
import 'package:mangayomi/models/manga.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
part 'library_filter_provider.g.dart';
|
||||||
|
|
||||||
|
/// Pre-fetches all downloaded chapter IDs in a single Isar query.
|
||||||
|
/// Returns a [Set<int>] for O(1) lookup instead of per-chapter queries.
|
||||||
|
@riverpod
|
||||||
|
Set<int> downloadedChapterIds(Ref ref) {
|
||||||
|
final downloads = isar.downloads
|
||||||
|
.filter()
|
||||||
|
.isDownloadEqualTo(true)
|
||||||
|
.idProperty()
|
||||||
|
.findAllSync();
|
||||||
|
return downloads.whereType<int>().toSet();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filters and sorts a list of [Manga] based on library filter/sort settings.
|
||||||
|
///
|
||||||
|
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
|
||||||
|
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
|
||||||
|
@riverpod
|
||||||
|
List<Manga> filteredLibraryManga(
|
||||||
|
Ref ref, {
|
||||||
|
required List<Manga> data,
|
||||||
|
required int downloadFilterType,
|
||||||
|
required int unreadFilterType,
|
||||||
|
required int startedFilterType,
|
||||||
|
required int bookmarkedFilterType,
|
||||||
|
required int sortType,
|
||||||
|
required bool downloadedOnly,
|
||||||
|
required String searchQuery,
|
||||||
|
required bool ignoreFiltersOnSearch,
|
||||||
|
}) {
|
||||||
|
final downloadedIds = ref.watch(downloadedChapterIdsProvider);
|
||||||
|
|
||||||
|
return _filterAndSortManga(
|
||||||
|
data: data,
|
||||||
|
downloadFilterType: downloadFilterType,
|
||||||
|
unreadFilterType: unreadFilterType,
|
||||||
|
startedFilterType: startedFilterType,
|
||||||
|
bookmarkedFilterType: bookmarkedFilterType,
|
||||||
|
sortType: sortType,
|
||||||
|
downloadedOnly: downloadedOnly,
|
||||||
|
searchQuery: searchQuery,
|
||||||
|
ignoreFiltersOnSearch: ignoreFiltersOnSearch,
|
||||||
|
downloadedIds: downloadedIds,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _matchesSearchQuery(Manga manga, String query) {
|
||||||
|
final keywords = query
|
||||||
|
.toLowerCase()
|
||||||
|
.split(',')
|
||||||
|
.map((k) => k.trim())
|
||||||
|
.where((k) => k.isNotEmpty);
|
||||||
|
|
||||||
|
return keywords.any(
|
||||||
|
(keyword) =>
|
||||||
|
(manga.name?.toLowerCase().contains(keyword) ?? false) ||
|
||||||
|
(manga.source?.toLowerCase().contains(keyword) ?? false) ||
|
||||||
|
(manga.genre?.any((g) => g.toLowerCase().contains(keyword)) ?? false),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<Manga> _filterAndSortManga({
|
||||||
|
required List<Manga> data,
|
||||||
|
required int downloadFilterType,
|
||||||
|
required int unreadFilterType,
|
||||||
|
required int startedFilterType,
|
||||||
|
required int bookmarkedFilterType,
|
||||||
|
required int sortType,
|
||||||
|
required bool downloadedOnly,
|
||||||
|
required String searchQuery,
|
||||||
|
required bool ignoreFiltersOnSearch,
|
||||||
|
required Set<int> downloadedIds,
|
||||||
|
}) {
|
||||||
|
List<Manga> mangas;
|
||||||
|
|
||||||
|
// Skip all filters, just do search
|
||||||
|
if (searchQuery.isNotEmpty && ignoreFiltersOnSearch) {
|
||||||
|
mangas = data
|
||||||
|
.where((element) => _matchesSearchQuery(element, searchQuery))
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
mangas = data.where((element) {
|
||||||
|
// Filter by download — uses Set lookup instead of per-chapter Isar query
|
||||||
|
if (downloadFilterType == 1 || downloadedOnly) {
|
||||||
|
final hasDownloaded = element.chapters.any(
|
||||||
|
(chap) => chap.id != null && downloadedIds.contains(chap.id),
|
||||||
|
);
|
||||||
|
if (!hasDownloaded) return false;
|
||||||
|
} else if (downloadFilterType == 2) {
|
||||||
|
final allNotDownloaded = element.chapters.every(
|
||||||
|
(chap) => chap.id == null || !downloadedIds.contains(chap.id),
|
||||||
|
);
|
||||||
|
if (!allNotDownloaded) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by unread or started
|
||||||
|
if (unreadFilterType == 1 || startedFilterType == 1) {
|
||||||
|
final hasUnread = element.chapters.any((chap) => !chap.isRead!);
|
||||||
|
if (!hasUnread) return false;
|
||||||
|
} else if (unreadFilterType == 2 || startedFilterType == 2) {
|
||||||
|
final allRead = element.chapters.every((chap) => chap.isRead!);
|
||||||
|
if (!allRead) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter by bookmarked
|
||||||
|
if (bookmarkedFilterType == 1) {
|
||||||
|
final hasBookmarked = element.chapters.any(
|
||||||
|
(chap) => chap.isBookmarked!,
|
||||||
|
);
|
||||||
|
if (!hasBookmarked) return false;
|
||||||
|
} else if (bookmarkedFilterType == 2) {
|
||||||
|
final allNotBookmarked = element.chapters.every(
|
||||||
|
(chap) => !chap.isBookmarked!,
|
||||||
|
);
|
||||||
|
if (!allNotBookmarked) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search filter
|
||||||
|
if (searchQuery.isNotEmpty) {
|
||||||
|
if (!_matchesSearchQuery(element, searchQuery)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort
|
||||||
|
mangas.sort((a, b) {
|
||||||
|
switch (sortType) {
|
||||||
|
case 0:
|
||||||
|
return a.name!.compareTo(b.name!);
|
||||||
|
case 1:
|
||||||
|
return a.lastRead!.compareTo(b.lastRead!);
|
||||||
|
case 2:
|
||||||
|
return a.lastUpdate?.compareTo(b.lastUpdate ?? 0) ?? 0;
|
||||||
|
case 3:
|
||||||
|
return a.chapters
|
||||||
|
.where((e) => !e.isRead!)
|
||||||
|
.length
|
||||||
|
.compareTo(b.chapters.where((e) => !e.isRead!).length);
|
||||||
|
case 4:
|
||||||
|
return a.chapters.length.compareTo(b.chapters.length);
|
||||||
|
case 5:
|
||||||
|
return (a.chapters.lastOrNull?.dateUpload ?? "").compareTo(
|
||||||
|
b.chapters.lastOrNull?.dateUpload ?? "",
|
||||||
|
);
|
||||||
|
case 6:
|
||||||
|
return a.dateAdded?.compareTo(b.dateAdded ?? 0) ?? 0;
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return mangas;
|
||||||
|
}
|
||||||
231
lib/modules/library/providers/library_filter_provider.g.dart
Normal file
231
lib/modules/library/providers/library_filter_provider.g.dart
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'library_filter_provider.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// RiverpodGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
// ignore_for_file: type=lint, type=warning
|
||||||
|
/// Pre-fetches all downloaded chapter IDs in a single Isar query.
|
||||||
|
/// Returns a [Set<int>] for O(1) lookup instead of per-chapter queries.
|
||||||
|
|
||||||
|
@ProviderFor(downloadedChapterIds)
|
||||||
|
final downloadedChapterIdsProvider = DownloadedChapterIdsProvider._();
|
||||||
|
|
||||||
|
/// Pre-fetches all downloaded chapter IDs in a single Isar query.
|
||||||
|
/// Returns a [Set<int>] for O(1) lookup instead of per-chapter queries.
|
||||||
|
|
||||||
|
final class DownloadedChapterIdsProvider
|
||||||
|
extends $FunctionalProvider<Set<int>, Set<int>, Set<int>>
|
||||||
|
with $Provider<Set<int>> {
|
||||||
|
/// Pre-fetches all downloaded chapter IDs in a single Isar query.
|
||||||
|
/// Returns a [Set<int>] for O(1) lookup instead of per-chapter queries.
|
||||||
|
DownloadedChapterIdsProvider._()
|
||||||
|
: super(
|
||||||
|
from: null,
|
||||||
|
argument: null,
|
||||||
|
retry: null,
|
||||||
|
name: r'downloadedChapterIdsProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$downloadedChapterIdsHash();
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<Set<int>> $createElement($ProviderPointer pointer) =>
|
||||||
|
$ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Set<int> create(Ref ref) {
|
||||||
|
return downloadedChapterIds(ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(Set<int> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<Set<int>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$downloadedChapterIdsHash() =>
|
||||||
|
r'a51ff78fb0ad2548c719d1ca400ae474fc01e683';
|
||||||
|
|
||||||
|
/// Filters and sorts a list of [Manga] based on library filter/sort settings.
|
||||||
|
///
|
||||||
|
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
|
||||||
|
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
|
||||||
|
|
||||||
|
@ProviderFor(filteredLibraryManga)
|
||||||
|
final filteredLibraryMangaProvider = FilteredLibraryMangaFamily._();
|
||||||
|
|
||||||
|
/// Filters and sorts a list of [Manga] based on library filter/sort settings.
|
||||||
|
///
|
||||||
|
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
|
||||||
|
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
|
||||||
|
|
||||||
|
final class FilteredLibraryMangaProvider
|
||||||
|
extends $FunctionalProvider<List<Manga>, List<Manga>, List<Manga>>
|
||||||
|
with $Provider<List<Manga>> {
|
||||||
|
/// Filters and sorts a list of [Manga] based on library filter/sort settings.
|
||||||
|
///
|
||||||
|
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
|
||||||
|
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
|
||||||
|
FilteredLibraryMangaProvider._({
|
||||||
|
required FilteredLibraryMangaFamily super.from,
|
||||||
|
required ({
|
||||||
|
List<Manga> data,
|
||||||
|
int downloadFilterType,
|
||||||
|
int unreadFilterType,
|
||||||
|
int startedFilterType,
|
||||||
|
int bookmarkedFilterType,
|
||||||
|
int sortType,
|
||||||
|
bool downloadedOnly,
|
||||||
|
String searchQuery,
|
||||||
|
bool ignoreFiltersOnSearch,
|
||||||
|
})
|
||||||
|
super.argument,
|
||||||
|
}) : super(
|
||||||
|
retry: null,
|
||||||
|
name: r'filteredLibraryMangaProvider',
|
||||||
|
isAutoDispose: true,
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String debugGetCreateSourceHash() => _$filteredLibraryMangaHash();
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return r'filteredLibraryMangaProvider'
|
||||||
|
''
|
||||||
|
'$argument';
|
||||||
|
}
|
||||||
|
|
||||||
|
@$internal
|
||||||
|
@override
|
||||||
|
$ProviderElement<List<Manga>> $createElement($ProviderPointer pointer) =>
|
||||||
|
$ProviderElement(pointer);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Manga> create(Ref ref) {
|
||||||
|
final argument =
|
||||||
|
this.argument
|
||||||
|
as ({
|
||||||
|
List<Manga> data,
|
||||||
|
int downloadFilterType,
|
||||||
|
int unreadFilterType,
|
||||||
|
int startedFilterType,
|
||||||
|
int bookmarkedFilterType,
|
||||||
|
int sortType,
|
||||||
|
bool downloadedOnly,
|
||||||
|
String searchQuery,
|
||||||
|
bool ignoreFiltersOnSearch,
|
||||||
|
});
|
||||||
|
return filteredLibraryManga(
|
||||||
|
ref,
|
||||||
|
data: argument.data,
|
||||||
|
downloadFilterType: argument.downloadFilterType,
|
||||||
|
unreadFilterType: argument.unreadFilterType,
|
||||||
|
startedFilterType: argument.startedFilterType,
|
||||||
|
bookmarkedFilterType: argument.bookmarkedFilterType,
|
||||||
|
sortType: argument.sortType,
|
||||||
|
downloadedOnly: argument.downloadedOnly,
|
||||||
|
searchQuery: argument.searchQuery,
|
||||||
|
ignoreFiltersOnSearch: argument.ignoreFiltersOnSearch,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// {@macro riverpod.override_with_value}
|
||||||
|
Override overrideWithValue(List<Manga> value) {
|
||||||
|
return $ProviderOverride(
|
||||||
|
origin: this,
|
||||||
|
providerOverride: $SyncValueProvider<List<Manga>>(value),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
return other is FilteredLibraryMangaProvider && other.argument == argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode {
|
||||||
|
return argument.hashCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _$filteredLibraryMangaHash() =>
|
||||||
|
r'34cd87ea154cc617e85572ede503b81fb36f2a97';
|
||||||
|
|
||||||
|
/// Filters and sorts a list of [Manga] based on library filter/sort settings.
|
||||||
|
///
|
||||||
|
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
|
||||||
|
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
|
||||||
|
|
||||||
|
final class FilteredLibraryMangaFamily extends $Family
|
||||||
|
with
|
||||||
|
$FunctionalFamilyOverride<
|
||||||
|
List<Manga>,
|
||||||
|
({
|
||||||
|
List<Manga> data,
|
||||||
|
int downloadFilterType,
|
||||||
|
int unreadFilterType,
|
||||||
|
int startedFilterType,
|
||||||
|
int bookmarkedFilterType,
|
||||||
|
int sortType,
|
||||||
|
bool downloadedOnly,
|
||||||
|
String searchQuery,
|
||||||
|
bool ignoreFiltersOnSearch,
|
||||||
|
})
|
||||||
|
> {
|
||||||
|
FilteredLibraryMangaFamily._()
|
||||||
|
: super(
|
||||||
|
retry: null,
|
||||||
|
name: r'filteredLibraryMangaProvider',
|
||||||
|
dependencies: null,
|
||||||
|
$allTransitiveDependencies: null,
|
||||||
|
isAutoDispose: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Filters and sorts a list of [Manga] based on library filter/sort settings.
|
||||||
|
///
|
||||||
|
/// Uses [downloadedChapterIds] for O(1) download lookups instead of
|
||||||
|
/// per-chapter Isar queries (previous behavior was O(chapters × manga)).
|
||||||
|
|
||||||
|
FilteredLibraryMangaProvider call({
|
||||||
|
required List<Manga> data,
|
||||||
|
required int downloadFilterType,
|
||||||
|
required int unreadFilterType,
|
||||||
|
required int startedFilterType,
|
||||||
|
required int bookmarkedFilterType,
|
||||||
|
required int sortType,
|
||||||
|
required bool downloadedOnly,
|
||||||
|
required String searchQuery,
|
||||||
|
required bool ignoreFiltersOnSearch,
|
||||||
|
}) => FilteredLibraryMangaProvider._(
|
||||||
|
argument: (
|
||||||
|
data: data,
|
||||||
|
downloadFilterType: downloadFilterType,
|
||||||
|
unreadFilterType: unreadFilterType,
|
||||||
|
startedFilterType: startedFilterType,
|
||||||
|
bookmarkedFilterType: bookmarkedFilterType,
|
||||||
|
sortType: sortType,
|
||||||
|
downloadedOnly: downloadedOnly,
|
||||||
|
searchQuery: searchQuery,
|
||||||
|
ignoreFiltersOnSearch: ignoreFiltersOnSearch,
|
||||||
|
),
|
||||||
|
from: this,
|
||||||
|
);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => r'filteredLibraryMangaProvider';
|
||||||
|
}
|
||||||
283
lib/modules/library/widgets/library_app_bar.dart
Normal file
283
lib/modules/library/widgets/library_app_bar.dart
Normal file
|
|
@ -0,0 +1,283 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:mangayomi/models/manga.dart';
|
||||||
|
import 'package:mangayomi/models/settings.dart';
|
||||||
|
import 'package:mangayomi/modules/library/library_screen.dart';
|
||||||
|
import 'package:mangayomi/modules/library/providers/isar_providers.dart';
|
||||||
|
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
|
||||||
|
import 'package:mangayomi/modules/library/widgets/library_dialogs.dart';
|
||||||
|
import 'package:mangayomi/modules/library/widgets/library_settings_sheet.dart';
|
||||||
|
import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart';
|
||||||
|
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/error_text.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||||
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||||
|
import 'package:mangayomi/services/library_updater.dart';
|
||||||
|
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:mangayomi/utils/global_style.dart';
|
||||||
|
import 'package:mangayomi/utils/item_type_localization.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
|
||||||
|
|
||||||
|
/// AppBar for the library screen.
|
||||||
|
///
|
||||||
|
/// Handles search mode, long-press selection mode, and the popup menu.
|
||||||
|
class LibraryAppBar extends ConsumerWidget implements PreferredSizeWidget {
|
||||||
|
final ItemType itemType;
|
||||||
|
final bool isNotFiltering;
|
||||||
|
final bool showNumbersOfItems;
|
||||||
|
final int numberOfItems;
|
||||||
|
final List<Manga> entries;
|
||||||
|
final bool isCategory;
|
||||||
|
final int? categoryId;
|
||||||
|
final Settings settings;
|
||||||
|
final bool isSearch;
|
||||||
|
final bool ignoreFiltersOnSearch;
|
||||||
|
final TextEditingController textEditingController;
|
||||||
|
final VoidCallback onSearchToggle;
|
||||||
|
final VoidCallback onSearchClear;
|
||||||
|
final ValueChanged<bool> onIgnoreFiltersChanged;
|
||||||
|
final TickerProvider vsync;
|
||||||
|
|
||||||
|
const LibraryAppBar({
|
||||||
|
super.key,
|
||||||
|
required this.itemType,
|
||||||
|
required this.isNotFiltering,
|
||||||
|
required this.showNumbersOfItems,
|
||||||
|
required this.numberOfItems,
|
||||||
|
required this.entries,
|
||||||
|
required this.isCategory,
|
||||||
|
required this.categoryId,
|
||||||
|
required this.settings,
|
||||||
|
required this.isSearch,
|
||||||
|
required this.ignoreFiltersOnSearch,
|
||||||
|
required this.textEditingController,
|
||||||
|
required this.onSearchToggle,
|
||||||
|
required this.onSearchClear,
|
||||||
|
required this.onIgnoreFiltersChanged,
|
||||||
|
required this.vsync,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Size get preferredSize => Size.fromHeight(AppBar().preferredSize.height);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isLongPressed = ref.watch(isLongPressedStateProvider);
|
||||||
|
final mangaIdsList = ref.watch(mangasListStateProvider);
|
||||||
|
final manga = categoryId == null
|
||||||
|
? ref.watch(
|
||||||
|
getAllMangaWithoutCategoriesStreamProvider(itemType: itemType),
|
||||||
|
)
|
||||||
|
: ref.watch(
|
||||||
|
getAllMangaStreamProvider(
|
||||||
|
categoryId: categoryId,
|
||||||
|
itemType: itemType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
final isMobile = Platform.isIOS || Platform.isAndroid;
|
||||||
|
|
||||||
|
if (isLongPressed) {
|
||||||
|
return manga.when(
|
||||||
|
data: (data) => _SelectionAppBar(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaIdsList: mangaIdsList,
|
||||||
|
data: data,
|
||||||
|
),
|
||||||
|
error: (error, _) => ErrorText(error),
|
||||||
|
loading: () => const ProgressCenter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return AppBar(
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
title: isSearch
|
||||||
|
? null
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
itemType.localized(l10n),
|
||||||
|
style: TextStyle(color: Theme.of(context).hintColor),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
if (showNumbersOfItems)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 3),
|
||||||
|
child: Badge(
|
||||||
|
backgroundColor: Theme.of(context).focusColor,
|
||||||
|
label: Text(
|
||||||
|
numberOfItems.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Theme.of(context).textTheme.bodySmall!.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
isSearch
|
||||||
|
? SeachFormTextField(
|
||||||
|
onChanged: (_) => onSearchClear(),
|
||||||
|
onPressed: onSearchToggle,
|
||||||
|
controller: textEditingController,
|
||||||
|
onSuffixPressed: () {
|
||||||
|
textEditingController.clear();
|
||||||
|
onSearchClear();
|
||||||
|
},
|
||||||
|
)
|
||||||
|
: IconButton(
|
||||||
|
splashRadius: 20,
|
||||||
|
onPressed: () {
|
||||||
|
textEditingController.clear();
|
||||||
|
onSearchToggle();
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
),
|
||||||
|
// Checkbox when searching library to ignore filters
|
||||||
|
if (isSearch)
|
||||||
|
Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
isMobile
|
||||||
|
? l10n.ignore_filters.replaceFirst(' ', '\n')
|
||||||
|
: l10n.ignore_filters.replaceAll('\n', ''),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
Checkbox(
|
||||||
|
value: ignoreFiltersOnSearch,
|
||||||
|
onChanged: (val) {
|
||||||
|
onIgnoreFiltersChanged(val ?? false);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
splashRadius: 20,
|
||||||
|
onPressed: () {
|
||||||
|
showLibrarySettingsSheet(
|
||||||
|
context: context,
|
||||||
|
vsync: vsync,
|
||||||
|
settings: settings,
|
||||||
|
itemType: itemType,
|
||||||
|
entries: entries,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
icon: Icon(
|
||||||
|
Icons.filter_list_sharp,
|
||||||
|
color: isNotFiltering ? null : Colors.yellow,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
PopupMenuButton(
|
||||||
|
popUpAnimationStyle: popupAnimationStyle,
|
||||||
|
itemBuilder: (context) {
|
||||||
|
return [
|
||||||
|
PopupMenuItem<int>(
|
||||||
|
value: 0,
|
||||||
|
child: Text(context.l10n.update_library),
|
||||||
|
),
|
||||||
|
PopupMenuItem<int>(value: 1, child: Text(l10n.open_random_entry)),
|
||||||
|
PopupMenuItem<int>(value: 2, child: Text(l10n.import)),
|
||||||
|
if (itemType == ItemType.anime)
|
||||||
|
PopupMenuItem<int>(value: 3, child: Text(l10n.torrent_stream)),
|
||||||
|
];
|
||||||
|
},
|
||||||
|
onSelected: (value) {
|
||||||
|
if (value == 0) {
|
||||||
|
manga.whenData((value) {
|
||||||
|
updateLibrary(
|
||||||
|
ref: ref,
|
||||||
|
context: context,
|
||||||
|
mangaList: value,
|
||||||
|
itemType: itemType,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else if (value == 1) {
|
||||||
|
manga.whenData((value) {
|
||||||
|
var randomManga = (value..shuffle()).first;
|
||||||
|
pushToMangaReaderDetail(
|
||||||
|
ref: ref,
|
||||||
|
archiveId: randomManga.isLocalArchive ?? false
|
||||||
|
? randomManga.id
|
||||||
|
: null,
|
||||||
|
context: context,
|
||||||
|
lang: randomManga.lang!,
|
||||||
|
mangaM: randomManga,
|
||||||
|
source: randomManga.source!,
|
||||||
|
sourceId: randomManga.sourceId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
} else if (value == 2) {
|
||||||
|
showImportLocalDialog(context, itemType);
|
||||||
|
} else if (value == 3 && itemType == ItemType.anime) {
|
||||||
|
addTorrent(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// AppBar shown when items are long-pressed for bulk selection.
|
||||||
|
class _SelectionAppBar extends ConsumerWidget {
|
||||||
|
final ItemType itemType;
|
||||||
|
final List<int> mangaIdsList;
|
||||||
|
final List<Manga> data;
|
||||||
|
|
||||||
|
const _SelectionAppBar({
|
||||||
|
required this.itemType,
|
||||||
|
required this.mangaIdsList,
|
||||||
|
required this.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final isLongPressed = ref.watch(isLongPressedStateProvider);
|
||||||
|
return Container(
|
||||||
|
color: Theme.of(context).scaffoldBackgroundColor,
|
||||||
|
child: AppBar(
|
||||||
|
title: Text(mangaIdsList.length.toString()),
|
||||||
|
backgroundColor: context.primaryColor.withValues(alpha: 0.2),
|
||||||
|
leading: IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
ref.read(mangasListStateProvider.notifier).clear();
|
||||||
|
ref
|
||||||
|
.read(isLongPressedStateProvider.notifier)
|
||||||
|
.update(!isLongPressed);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.clear),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
for (var manga in data) {
|
||||||
|
ref.read(mangasListStateProvider.notifier).selectAll(manga);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.select_all),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (data.length == mangaIdsList.length) {
|
||||||
|
for (var manga in data) {
|
||||||
|
ref.read(mangasListStateProvider.notifier).selectSome(manga);
|
||||||
|
}
|
||||||
|
ref.read(isLongPressedStateProvider.notifier).update(false);
|
||||||
|
} else {
|
||||||
|
for (var manga in data) {
|
||||||
|
ref.read(mangasListStateProvider.notifier).selectSome(manga);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.flip_to_back_rounded),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
213
lib/modules/library/widgets/library_body.dart
Normal file
213
lib/modules/library/widgets/library_body.dart
Normal file
|
|
@ -0,0 +1,213 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:mangayomi/models/manga.dart';
|
||||||
|
import 'package:mangayomi/models/settings.dart';
|
||||||
|
import 'package:mangayomi/modules/library/providers/isar_providers.dart';
|
||||||
|
import 'package:mangayomi/modules/library/providers/library_filter_provider.dart';
|
||||||
|
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
|
||||||
|
import 'package:mangayomi/modules/library/widgets/library_gridview_widget.dart';
|
||||||
|
import 'package:mangayomi/modules/library/widgets/library_listview_widget.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/error_text.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||||
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||||
|
import 'package:mangayomi/services/library_updater.dart';
|
||||||
|
|
||||||
|
/// Displays the library body content for a given category (or uncategorized).
|
||||||
|
///
|
||||||
|
/// Uses [filteredLibraryMangaProvider] for cached, optimized filtering
|
||||||
|
/// instead of calling _filterAndSortManga inline (which was O(N*M) due to
|
||||||
|
/// per-chapter Isar queries).
|
||||||
|
class LibraryBody extends ConsumerWidget {
|
||||||
|
final ItemType itemType;
|
||||||
|
final int? categoryId;
|
||||||
|
final bool withoutCategories;
|
||||||
|
final int downloadFilterType;
|
||||||
|
final int unreadFilterType;
|
||||||
|
final int startedFilterType;
|
||||||
|
final int bookmarkedFilterType;
|
||||||
|
final bool reverse;
|
||||||
|
final bool downloadedChapter;
|
||||||
|
final bool continueReaderBtn;
|
||||||
|
final bool localSource;
|
||||||
|
final bool language;
|
||||||
|
final DisplayType displayType;
|
||||||
|
final Settings settings;
|
||||||
|
final bool downloadedOnly;
|
||||||
|
final String searchQuery;
|
||||||
|
final bool ignoreFiltersOnSearch;
|
||||||
|
|
||||||
|
const LibraryBody({
|
||||||
|
super.key,
|
||||||
|
required this.itemType,
|
||||||
|
this.categoryId,
|
||||||
|
this.withoutCategories = false,
|
||||||
|
required this.downloadFilterType,
|
||||||
|
required this.unreadFilterType,
|
||||||
|
required this.startedFilterType,
|
||||||
|
required this.bookmarkedFilterType,
|
||||||
|
required this.reverse,
|
||||||
|
required this.downloadedChapter,
|
||||||
|
required this.continueReaderBtn,
|
||||||
|
required this.localSource,
|
||||||
|
required this.language,
|
||||||
|
required this.displayType,
|
||||||
|
required this.settings,
|
||||||
|
required this.downloadedOnly,
|
||||||
|
required this.searchQuery,
|
||||||
|
required this.ignoreFiltersOnSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
final sortType = ref
|
||||||
|
.watch(
|
||||||
|
sortLibraryMangaStateProvider(itemType: itemType, settings: settings),
|
||||||
|
)
|
||||||
|
.index;
|
||||||
|
final mangaIdsList = ref.watch(mangasListStateProvider);
|
||||||
|
|
||||||
|
// Choose the right data stream based on whether this is a category tab
|
||||||
|
final mangaStream = withoutCategories
|
||||||
|
? ref.watch(
|
||||||
|
getAllMangaWithoutCategoriesStreamProvider(itemType: itemType),
|
||||||
|
)
|
||||||
|
: ref.watch(
|
||||||
|
getAllMangaStreamProvider(
|
||||||
|
categoryId: categoryId,
|
||||||
|
itemType: itemType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return mangaStream.when(
|
||||||
|
data: (data) {
|
||||||
|
// Use the cached filtering provider instead of inline filtering
|
||||||
|
final entries = ref.watch(
|
||||||
|
filteredLibraryMangaProvider(
|
||||||
|
data: data,
|
||||||
|
downloadFilterType: downloadFilterType,
|
||||||
|
unreadFilterType: unreadFilterType,
|
||||||
|
startedFilterType: startedFilterType,
|
||||||
|
bookmarkedFilterType: bookmarkedFilterType,
|
||||||
|
sortType: sortType ?? 0,
|
||||||
|
downloadedOnly: downloadedOnly,
|
||||||
|
searchQuery: searchQuery,
|
||||||
|
ignoreFiltersOnSearch: ignoreFiltersOnSearch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entries.isEmpty) {
|
||||||
|
return Center(child: Text(l10n.empty_library));
|
||||||
|
}
|
||||||
|
|
||||||
|
final entriesManga = reverse ? entries.reversed.toList() : entries;
|
||||||
|
return RefreshIndicator(
|
||||||
|
onRefresh: () async {
|
||||||
|
await updateLibrary(
|
||||||
|
ref: ref,
|
||||||
|
context: context,
|
||||||
|
mangaList: data,
|
||||||
|
itemType: itemType,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: displayType == DisplayType.list
|
||||||
|
? LibraryListViewWidget(
|
||||||
|
entriesManga: entriesManga,
|
||||||
|
continueReaderBtn: continueReaderBtn,
|
||||||
|
downloadedChapter: downloadedChapter,
|
||||||
|
language: language,
|
||||||
|
mangaIdsList: mangaIdsList,
|
||||||
|
localSource: localSource,
|
||||||
|
)
|
||||||
|
: LibraryGridViewWidget(
|
||||||
|
entriesManga: entriesManga,
|
||||||
|
isCoverOnlyGrid: !(displayType == DisplayType.compactGrid),
|
||||||
|
isComfortableGrid: displayType == DisplayType.comfortableGrid,
|
||||||
|
continueReaderBtn: continueReaderBtn,
|
||||||
|
downloadedChapter: downloadedChapter,
|
||||||
|
language: language,
|
||||||
|
mangaIdsList: mangaIdsList,
|
||||||
|
localSource: localSource,
|
||||||
|
itemType: itemType,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, _) => ErrorText(error),
|
||||||
|
loading: () => const ProgressCenter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Badge showing the number of items in a category tab.
|
||||||
|
///
|
||||||
|
/// Uses the cached filtering provider for consistent results without
|
||||||
|
/// re-running the filter logic.
|
||||||
|
class CategoryBadge extends ConsumerWidget {
|
||||||
|
final ItemType itemType;
|
||||||
|
final int categoryId;
|
||||||
|
final int downloadFilterType;
|
||||||
|
final int unreadFilterType;
|
||||||
|
final int startedFilterType;
|
||||||
|
final int bookmarkedFilterType;
|
||||||
|
final Settings settings;
|
||||||
|
final bool downloadedOnly;
|
||||||
|
final String searchQuery;
|
||||||
|
final bool ignoreFiltersOnSearch;
|
||||||
|
|
||||||
|
const CategoryBadge({
|
||||||
|
super.key,
|
||||||
|
required this.itemType,
|
||||||
|
required this.categoryId,
|
||||||
|
required this.downloadFilterType,
|
||||||
|
required this.unreadFilterType,
|
||||||
|
required this.startedFilterType,
|
||||||
|
required this.bookmarkedFilterType,
|
||||||
|
required this.settings,
|
||||||
|
required this.downloadedOnly,
|
||||||
|
required this.searchQuery,
|
||||||
|
required this.ignoreFiltersOnSearch,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final mangas = ref.watch(
|
||||||
|
getAllMangaStreamProvider(categoryId: categoryId, itemType: itemType),
|
||||||
|
);
|
||||||
|
final sortType = ref
|
||||||
|
.watch(
|
||||||
|
sortLibraryMangaStateProvider(itemType: itemType, settings: settings),
|
||||||
|
)
|
||||||
|
.index;
|
||||||
|
|
||||||
|
return mangas.when(
|
||||||
|
data: (data) {
|
||||||
|
final filtered = ref.watch(
|
||||||
|
filteredLibraryMangaProvider(
|
||||||
|
data: data,
|
||||||
|
downloadFilterType: downloadFilterType,
|
||||||
|
unreadFilterType: unreadFilterType,
|
||||||
|
startedFilterType: startedFilterType,
|
||||||
|
bookmarkedFilterType: bookmarkedFilterType,
|
||||||
|
sortType: sortType ?? 0,
|
||||||
|
downloadedOnly: downloadedOnly,
|
||||||
|
searchQuery: searchQuery,
|
||||||
|
ignoreFiltersOnSearch: ignoreFiltersOnSearch,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return CircleAvatar(
|
||||||
|
backgroundColor: Theme.of(context).focusColor,
|
||||||
|
radius: 8,
|
||||||
|
child: Text(
|
||||||
|
filtered.length.toString(),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
color: Theme.of(context).textTheme.bodySmall!.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
error: (error, _) => ErrorText(error),
|
||||||
|
loading: () => const ProgressCenter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
364
lib/modules/library/widgets/library_dialogs.dart
Normal file
364
lib/modules/library/widgets/library_dialogs.dart
Normal file
|
|
@ -0,0 +1,364 @@
|
||||||
|
import 'dart:io';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:isar_community/isar.dart';
|
||||||
|
import 'package:mangayomi/main.dart';
|
||||||
|
import 'package:mangayomi/models/chapter.dart';
|
||||||
|
import 'package:mangayomi/models/download.dart';
|
||||||
|
import 'package:mangayomi/models/history.dart';
|
||||||
|
import 'package:mangayomi/models/manga.dart';
|
||||||
|
import 'package:mangayomi/models/update.dart';
|
||||||
|
import 'package:mangayomi/models/changed.dart';
|
||||||
|
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
|
||||||
|
import 'package:mangayomi/modules/library/providers/local_archive.dart';
|
||||||
|
import 'package:mangayomi/modules/manga/detail/providers/state_providers.dart';
|
||||||
|
import 'package:mangayomi/modules/manga/detail/widgets/chapter_filter_list_tile_widget.dart';
|
||||||
|
import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/progress_center.dart';
|
||||||
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||||
|
import 'package:mangayomi/providers/storage_provider.dart';
|
||||||
|
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||||
|
import 'package:mangayomi/utils/extensions/string_extensions.dart';
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
/// Shows a dialog for deleting selected manga from library and/or device.
|
||||||
|
void showDeleteMangaDialog({
|
||||||
|
required BuildContext context,
|
||||||
|
required WidgetRef ref,
|
||||||
|
required ItemType itemType,
|
||||||
|
}) {
|
||||||
|
List<int> fromLibList = [];
|
||||||
|
List<int> downloadedChapsList = [];
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) {
|
||||||
|
return Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
final mangaIdsList = ref.watch(mangasListStateProvider);
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
final List<Manga> mangasList = [];
|
||||||
|
for (var id in mangaIdsList) {
|
||||||
|
mangasList.add(isar.mangas.getSync(id)!);
|
||||||
|
}
|
||||||
|
return StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(l10n.remove),
|
||||||
|
content: SizedBox(
|
||||||
|
height: 100,
|
||||||
|
width: context.width(0.8),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: l10n.from_library,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (fromLibList == mangaIdsList) {
|
||||||
|
fromLibList = [];
|
||||||
|
} else {
|
||||||
|
fromLibList = mangaIdsList;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
type: fromLibList.isNotEmpty ? 1 : 0,
|
||||||
|
),
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: itemType != ItemType.anime
|
||||||
|
? l10n.downloaded_chapters
|
||||||
|
: l10n.downloaded_episodes,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
if (downloadedChapsList == mangaIdsList) {
|
||||||
|
downloadedChapsList = [];
|
||||||
|
} else {
|
||||||
|
downloadedChapsList = mangaIdsList;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
type: downloadedChapsList.isNotEmpty ? 1 : 0,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () async {
|
||||||
|
// From Library
|
||||||
|
if (fromLibList.isNotEmpty) {
|
||||||
|
isar.writeTxnSync(() {
|
||||||
|
for (var manga in mangasList) {
|
||||||
|
if (manga.isLocalArchive ?? false) {
|
||||||
|
_removeImport(ref, manga);
|
||||||
|
} else {
|
||||||
|
manga.favorite = false;
|
||||||
|
manga.updatedAt =
|
||||||
|
DateTime.now().millisecondsSinceEpoch;
|
||||||
|
isar.mangas.putSync(manga);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Downloaded Chapters
|
||||||
|
if (downloadedChapsList.isNotEmpty) {
|
||||||
|
for (var manga in mangasList) {
|
||||||
|
if (!(manga.isLocalArchive ?? false)) {
|
||||||
|
String mangaDirectory = "";
|
||||||
|
if (manga.isLocalArchive ?? false) {
|
||||||
|
mangaDirectory = _deleteImport(
|
||||||
|
manga,
|
||||||
|
mangaDirectory,
|
||||||
|
);
|
||||||
|
isar.writeTxnSync(() {
|
||||||
|
_removeImport(ref, manga);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
mangaDirectory = await _deleteDownload(
|
||||||
|
manga,
|
||||||
|
mangaDirectory,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (mangaDirectory.isNotEmpty) {
|
||||||
|
final path = Directory(mangaDirectory);
|
||||||
|
if (path.existsSync() &&
|
||||||
|
path.listSync().isEmpty) {
|
||||||
|
path.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ref.read(mangasListStateProvider.notifier).clear();
|
||||||
|
ref
|
||||||
|
.read(isLongPressedStateProvider.notifier)
|
||||||
|
.update(false);
|
||||||
|
if (context.mounted) {
|
||||||
|
Navigator.pop(context);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text(l10n.ok),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _removeImport(WidgetRef ref, Manga manga) {
|
||||||
|
final provider = ref.read(synchingProvider(syncId: 1).notifier);
|
||||||
|
final histories = isar.historys
|
||||||
|
.filter()
|
||||||
|
.mangaIdEqualTo(manga.id)
|
||||||
|
.findAllSync();
|
||||||
|
for (var history in histories) {
|
||||||
|
isar.historys.deleteSync(history.id!);
|
||||||
|
provider.addChangedPart(ActionType.removeHistory, history.id, "{}", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var chapter in manga.chapters) {
|
||||||
|
final updates = isar.updates
|
||||||
|
.filter()
|
||||||
|
.mangaIdEqualTo(chapter.mangaId)
|
||||||
|
.chapterNameEqualTo(chapter.name)
|
||||||
|
.findAllSync();
|
||||||
|
for (var update in updates) {
|
||||||
|
isar.updates.deleteSync(update.id!);
|
||||||
|
provider.addChangedPart(ActionType.removeUpdate, update.id, "{}", false);
|
||||||
|
}
|
||||||
|
isar.chapters.deleteSync(chapter.id!);
|
||||||
|
provider.addChangedPart(ActionType.removeChapter, chapter.id, "{}", false);
|
||||||
|
}
|
||||||
|
isar.mangas.deleteSync(manga.id!);
|
||||||
|
provider.addChangedPart(ActionType.removeItem, manga.id, "{}", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _deleteImport(Manga manga, String mangaDirectory) {
|
||||||
|
for (var chapter in manga.chapters) {
|
||||||
|
final path = chapter.archivePath;
|
||||||
|
if (path == null) continue;
|
||||||
|
final chapterFile = File(path);
|
||||||
|
if (mangaDirectory.isEmpty) {
|
||||||
|
mangaDirectory = p.dirname(path);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (chapterFile.existsSync()) {
|
||||||
|
chapterFile.deleteSync();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
}
|
||||||
|
return mangaDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _deleteDownload(Manga manga, String mangaDirectory) async {
|
||||||
|
final storageProvider = StorageProvider();
|
||||||
|
Directory? mangaDir;
|
||||||
|
final idsToDelete = <int>{};
|
||||||
|
final downloadedIds = (await isar.downloads.where().idProperty().findAll())
|
||||||
|
.toSet();
|
||||||
|
|
||||||
|
if (downloadedIds.isEmpty) return mangaDirectory;
|
||||||
|
|
||||||
|
for (var chapter in manga.chapters) {
|
||||||
|
if (chapter.id == null || !downloadedIds.contains(chapter.id)) continue;
|
||||||
|
|
||||||
|
mangaDir ??= await storageProvider.getMangaMainDirectory(chapter);
|
||||||
|
final chapterDir = await storageProvider.getMangaChapterDirectory(
|
||||||
|
chapter,
|
||||||
|
mangaMainDirectory: mangaDir,
|
||||||
|
);
|
||||||
|
File? file;
|
||||||
|
|
||||||
|
if (mangaDirectory.isEmpty) mangaDirectory = mangaDir!.path;
|
||||||
|
if (manga.itemType == ItemType.manga) {
|
||||||
|
file = File(p.join(mangaDir!.path, "${chapter.name}.cbz"));
|
||||||
|
} else if (manga.itemType == ItemType.anime) {
|
||||||
|
file = File(
|
||||||
|
p.join(
|
||||||
|
mangaDir!.path,
|
||||||
|
"${chapter.name!.replaceForbiddenCharacters(' ')}.mp4",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (file != null && file.existsSync()) {
|
||||||
|
file.deleteSync();
|
||||||
|
}
|
||||||
|
if (chapterDir!.existsSync()) {
|
||||||
|
chapterDir.deleteSync(recursive: true);
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
idsToDelete.add(chapter.id!);
|
||||||
|
}
|
||||||
|
if (idsToDelete.isNotEmpty) {
|
||||||
|
isar.writeTxnSync(() {
|
||||||
|
isar.downloads.deleteAllSync(idsToDelete.toList());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return mangaDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows a dialog for importing local files (zip, cbz, epub, video).
|
||||||
|
void showImportLocalDialog(BuildContext context, ItemType itemType) {
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
final filesText = switch (itemType) {
|
||||||
|
ItemType.manga => ".zip, .cbz",
|
||||||
|
ItemType.anime => ".mp4, .mkv, .avi, and more",
|
||||||
|
ItemType.novel => ".epub",
|
||||||
|
};
|
||||||
|
bool isLoading = false;
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: !isLoading,
|
||||||
|
builder: (context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: Text(l10n.import_local_file),
|
||||||
|
content: StatefulBuilder(
|
||||||
|
builder: (context, setState) {
|
||||||
|
return Consumer(
|
||||||
|
builder: (context, ref, child) {
|
||||||
|
return SizedBox(
|
||||||
|
height: 100,
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(3),
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
setState(() => isLoading = true);
|
||||||
|
await ref.watch(
|
||||||
|
importArchivesFromFileProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
null,
|
||||||
|
init: true,
|
||||||
|
).future,
|
||||||
|
);
|
||||||
|
setState(() => isLoading = false);
|
||||||
|
if (!context.mounted) return;
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.archive_outlined),
|
||||||
|
Text(
|
||||||
|
"${l10n.import_files} ( $filesText )",
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).textTheme.bodySmall!.color,
|
||||||
|
fontSize: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
if (isLoading)
|
||||||
|
Container(
|
||||||
|
width: context.width(1),
|
||||||
|
height: context.height(1),
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: UnconstrainedBox(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
color: Theme.of(
|
||||||
|
context,
|
||||||
|
).scaffoldBackgroundColor,
|
||||||
|
),
|
||||||
|
height: 50,
|
||||||
|
width: 50,
|
||||||
|
child: const Center(child: ProgressCenter()),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
child: Text(l10n.cancel),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 15),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
492
lib/modules/library/widgets/library_settings_sheet.dart
Normal file
492
lib/modules/library/widgets/library_settings_sheet.dart
Normal file
|
|
@ -0,0 +1,492 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
import 'package:mangayomi/models/manga.dart';
|
||||||
|
import 'package:mangayomi/models/settings.dart';
|
||||||
|
import 'package:mangayomi/modules/library/providers/library_state_provider.dart';
|
||||||
|
import 'package:mangayomi/modules/manga/detail/widgets/chapter_filter_list_tile_widget.dart';
|
||||||
|
import 'package:mangayomi/modules/manga/detail/widgets/chapter_sort_list_tile_widget.dart';
|
||||||
|
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
|
||||||
|
import 'package:mangayomi/providers/l10n_providers.dart';
|
||||||
|
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
|
||||||
|
|
||||||
|
/// Shows the library filter/sort/display settings sheet.
|
||||||
|
void showLibrarySettingsSheet({
|
||||||
|
required BuildContext context,
|
||||||
|
required TickerProvider vsync,
|
||||||
|
required Settings settings,
|
||||||
|
required ItemType itemType,
|
||||||
|
required List<Manga> entries,
|
||||||
|
}) {
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
customDraggableTabBar(
|
||||||
|
tabs: [
|
||||||
|
Tab(text: l10n.filter),
|
||||||
|
Tab(text: l10n.sort),
|
||||||
|
Tab(text: l10n.display),
|
||||||
|
],
|
||||||
|
children: [
|
||||||
|
_FilterTab(itemType: itemType, settings: settings, entries: entries),
|
||||||
|
_SortTab(itemType: itemType, settings: settings),
|
||||||
|
_DisplayTab(itemType: itemType, settings: settings),
|
||||||
|
],
|
||||||
|
context: context,
|
||||||
|
vsync: vsync,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Filter Tab ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _FilterTab extends ConsumerWidget {
|
||||||
|
final ItemType itemType;
|
||||||
|
final Settings settings;
|
||||||
|
final List<Manga> entries;
|
||||||
|
|
||||||
|
const _FilterTab({
|
||||||
|
required this.itemType,
|
||||||
|
required this.settings,
|
||||||
|
required this.entries,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: l10n.downloaded,
|
||||||
|
type: ref.watch(
|
||||||
|
mangaFilterDownloadedStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaList: entries,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
mangaFilterDownloadedStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaList: entries,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.update();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: itemType != ItemType.anime ? l10n.unread : l10n.unwatched,
|
||||||
|
type: ref.watch(
|
||||||
|
mangaFilterUnreadStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaList: entries,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
mangaFilterUnreadStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaList: entries,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.update();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: l10n.started,
|
||||||
|
type: ref.watch(
|
||||||
|
mangaFilterStartedStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaList: entries,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
mangaFilterStartedStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaList: entries,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.update();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: l10n.bookmarked,
|
||||||
|
type: ref.watch(
|
||||||
|
mangaFilterBookmarkedStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaList: entries,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
mangaFilterBookmarkedStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
mangaList: entries,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.update();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sort Tab ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _SortTab extends ConsumerWidget {
|
||||||
|
final ItemType itemType;
|
||||||
|
final Settings settings;
|
||||||
|
|
||||||
|
const _SortTab({required this.itemType, required this.settings});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final reverse = ref
|
||||||
|
.read(
|
||||||
|
sortLibraryMangaStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.isReverse();
|
||||||
|
final reverseChapter = ref.watch(
|
||||||
|
sortLibraryMangaStateProvider(itemType: itemType, settings: settings),
|
||||||
|
);
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
for (var i = 0; i < 7; i++)
|
||||||
|
ListTileChapterSort(
|
||||||
|
label: _getSortNameByIndex(i, context, itemType),
|
||||||
|
reverse: reverse,
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
sortLibraryMangaStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(i);
|
||||||
|
},
|
||||||
|
showLeading: reverseChapter.index == i,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getSortNameByIndex(int index, BuildContext context, ItemType itemType) {
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
return switch (index) {
|
||||||
|
0 => l10n.alphabetically,
|
||||||
|
1 => itemType != ItemType.anime ? l10n.last_read : l10n.last_watched,
|
||||||
|
2 => l10n.last_update_check,
|
||||||
|
3 => itemType != ItemType.anime ? l10n.unread_count : l10n.unwatched_count,
|
||||||
|
4 => itemType != ItemType.anime ? l10n.total_chapters : l10n.total_episodes,
|
||||||
|
5 => itemType != ItemType.anime ? l10n.latest_chapter : l10n.latest_episode,
|
||||||
|
_ => l10n.date_added,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Display Tab ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _DisplayTab extends ConsumerWidget {
|
||||||
|
final ItemType itemType;
|
||||||
|
final Settings settings;
|
||||||
|
|
||||||
|
const _DisplayTab({required this.itemType, required this.settings});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final l10n = l10nLocalizations(context)!;
|
||||||
|
final display = ref.watch(
|
||||||
|
libraryDisplayTypeStateProvider(itemType: itemType, settings: settings),
|
||||||
|
);
|
||||||
|
final displayV = ref.read(
|
||||||
|
libraryDisplayTypeStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
);
|
||||||
|
final showCategoryTabs = ref.watch(
|
||||||
|
libraryShowCategoryTabsStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final continueReaderBtn = ref.watch(
|
||||||
|
libraryShowContinueReadingButtonStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final showNumbersOfItems = ref.watch(
|
||||||
|
libraryShowNumbersOfItemsStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final downloadedChapter = ref.watch(
|
||||||
|
libraryDownloadedChaptersStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final language = ref.watch(
|
||||||
|
libraryLanguageStateProvider(itemType: itemType, settings: settings),
|
||||||
|
);
|
||||||
|
final localSource = ref.watch(
|
||||||
|
libraryLocalSourceStateProvider(itemType: itemType, settings: settings),
|
||||||
|
);
|
||||||
|
return SingleChildScrollView(
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Display mode
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 20, right: 20, top: 10),
|
||||||
|
child: Row(children: [Text(l10n.display_mode)]),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 20),
|
||||||
|
child: Wrap(
|
||||||
|
children: DisplayType.values.map((e) {
|
||||||
|
final selected = e == display;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 5),
|
||||||
|
child: ElevatedButton(
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
||||||
|
surfaceTintColor: Colors.transparent,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(10),
|
||||||
|
),
|
||||||
|
side: selected
|
||||||
|
? null
|
||||||
|
: BorderSide(
|
||||||
|
color: context.isLight
|
||||||
|
? Colors.black
|
||||||
|
: Colors.white,
|
||||||
|
width: 0.8,
|
||||||
|
),
|
||||||
|
shadowColor: Colors.transparent,
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: selected
|
||||||
|
? context.primaryColor.withValues(alpha: 0.2)
|
||||||
|
: Colors.transparent,
|
||||||
|
),
|
||||||
|
onPressed: () {
|
||||||
|
displayV.setLibraryDisplayType(e);
|
||||||
|
},
|
||||||
|
child: Text(
|
||||||
|
displayV.getLibraryDisplayTypeName(e, context),
|
||||||
|
style: TextStyle(
|
||||||
|
color: Theme.of(context).textTheme.bodyLarge!.color,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Grid size
|
||||||
|
_GridSizeSlider(itemType: itemType),
|
||||||
|
|
||||||
|
// Badges section
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 20, right: 20, top: 10),
|
||||||
|
child: Row(children: [Text(l10n.badges)]),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 5),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: itemType != ItemType.anime
|
||||||
|
? l10n.downloaded_chapters
|
||||||
|
: l10n.downloaded_episodes,
|
||||||
|
type: downloadedChapter ? 1 : 0,
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
libraryDownloadedChaptersStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(!downloadedChapter);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: l10n.language,
|
||||||
|
type: language ? 1 : 0,
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
libraryLanguageStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(!language);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: l10n.local_source,
|
||||||
|
type: localSource ? 1 : 0,
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
libraryLocalSourceStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(!localSource);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: itemType != ItemType.anime
|
||||||
|
? l10n.show_continue_reading_buttons
|
||||||
|
: l10n.show_continue_watching_buttons,
|
||||||
|
type: continueReaderBtn ? 1 : 0,
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
libraryShowContinueReadingButtonStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(!continueReaderBtn);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Tabs section
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 20, right: 20, top: 10),
|
||||||
|
child: Row(children: [Text(l10n.tabs)]),
|
||||||
|
),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 5),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: l10n.show_category_tabs,
|
||||||
|
type: showCategoryTabs ? 1 : 0,
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
libraryShowCategoryTabsStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(!showCategoryTabs);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
ListTileChapterFilter(
|
||||||
|
label: l10n.show_numbers_of_items,
|
||||||
|
type: showNumbersOfItems ? 1 : 0,
|
||||||
|
onTap: () {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
libraryShowNumbersOfItemsStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
settings: settings,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(!showNumbersOfItems);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Grid Size Slider ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
class _GridSizeSlider extends ConsumerWidget {
|
||||||
|
final ItemType itemType;
|
||||||
|
const _GridSizeSlider({required this.itemType});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final gridSize =
|
||||||
|
ref.watch(libraryGridSizeStateProvider(itemType: itemType)) ?? 0;
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8, right: 8, top: 10),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Text(context.l10n.grid_size),
|
||||||
|
Text(
|
||||||
|
gridSize == 0
|
||||||
|
? context.l10n.default0
|
||||||
|
: context.l10n.n_per_row(gridSize.toString()),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Flexible(
|
||||||
|
flex: 7,
|
||||||
|
child: SliderTheme(
|
||||||
|
data: SliderTheme.of(context).copyWith(
|
||||||
|
overlayShape: const RoundSliderOverlayShape(overlayRadius: 5.0),
|
||||||
|
),
|
||||||
|
child: Slider(
|
||||||
|
min: 0.0,
|
||||||
|
max: 7,
|
||||||
|
divisions: 7,
|
||||||
|
value: gridSize.toDouble(),
|
||||||
|
onChanged: (value) {
|
||||||
|
HapticFeedback.vibrate();
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
libraryGridSizeStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(value.toInt());
|
||||||
|
},
|
||||||
|
onChangeEnd: (value) {
|
||||||
|
ref
|
||||||
|
.read(
|
||||||
|
libraryGridSizeStateProvider(
|
||||||
|
itemType: itemType,
|
||||||
|
).notifier,
|
||||||
|
)
|
||||||
|
.set(value.toInt(), end: true);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,8 @@ class _MangaReaderDetailState extends ConsumerState<MangaReaderDetail> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 100));
|
// Wait for the widget tree to settle before loading detail
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
await ref.read(
|
await ref.read(
|
||||||
updateMangaDetailProvider(mangaId: widget.mangaId, isInit: true).future,
|
updateMangaDetailProvider(mangaId: widget.mangaId, isInit: true).future,
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -373,7 +373,8 @@ class ChaptersListttState extends _$ChaptersListttState {
|
||||||
}
|
}
|
||||||
|
|
||||||
void set(List<Chapter> chapters) async {
|
void set(List<Chapter> chapters) async {
|
||||||
await Future.delayed(const Duration(milliseconds: 10));
|
// Yield to the event loop to avoid setState during build
|
||||||
|
await Future(() {});
|
||||||
state = chapters;
|
state = chapters;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -958,7 +958,7 @@ final class ChaptersListttStateProvider
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$chaptersListttStateHash() =>
|
String _$chaptersListttStateHash() =>
|
||||||
r'f45ebd9a5b1fd86b279e263813098564830c2536';
|
r'55c0093bb370d4d103129eeca67e652a0241f2c0';
|
||||||
|
|
||||||
abstract class _$ChaptersListttState extends $Notifier<List<Chapter>> {
|
abstract class _$ChaptersListttState extends $Notifier<List<Chapter>> {
|
||||||
List<Chapter> build();
|
List<Chapter> build();
|
||||||
|
|
|
||||||
|
|
@ -64,7 +64,7 @@ final class UpdateMangaDetailProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$updateMangaDetailHash() => r'37da5f23f30126d15cedfaf42087f9ce11c3fc26';
|
String _$updateMangaDetailHash() => r'7071020d9d5dd6477875cc8fa0f226bd1d676620';
|
||||||
|
|
||||||
final class UpdateMangaDetailFamily extends $Family
|
final class UpdateMangaDetailFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,8 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
|
||||||
setState(() {
|
setState(() {
|
||||||
_query = "";
|
_query = "";
|
||||||
});
|
});
|
||||||
await Future.delayed(const Duration(milliseconds: 10));
|
// Yield a frame so the empty state is rendered before re-querying
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
setState(() {
|
setState(() {
|
||||||
_query = value;
|
_query = value;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,8 @@ class _TrackerWidgetSearchState extends ConsumerState<TrackerWidgetSearch> {
|
||||||
late List<TrackSearch>? tracks = [];
|
late List<TrackSearch>? tracks = [];
|
||||||
String? _errorMsg;
|
String? _errorMsg;
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
await Future.delayed(const Duration(microseconds: 100));
|
// Yield to microtask queue so initState completes before async work
|
||||||
|
await Future(() {});
|
||||||
try {
|
try {
|
||||||
tracks = await ref
|
tracks = await ref
|
||||||
.read(
|
.read(
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,8 @@ class _TrackerWidgetState extends ConsumerState<TrackerWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _init() async {
|
Future<void> _init() async {
|
||||||
await Future.delayed(const Duration(microseconds: 100));
|
// Yield to microtask queue so initState completes before async work
|
||||||
|
await Future(() {});
|
||||||
final findManga = await ref
|
final findManga = await ref
|
||||||
.read(
|
.read(
|
||||||
trackStateProvider(
|
trackStateProvider(
|
||||||
|
|
|
||||||
|
|
@ -136,7 +136,7 @@ final class DownloadChapterProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$downloadChapterHash() => r'c503cef46aa7083316b023400f0aa470ae3a3bc4';
|
String _$downloadChapterHash() => r'db235f856cf106c89d6124c361a51f2e312e9aa3';
|
||||||
|
|
||||||
final class DownloadChapterFamily extends $Family
|
final class DownloadChapterFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|
|
||||||
|
|
@ -147,7 +147,7 @@ final class ReaderControllerProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$readerControllerHash() => r'23eece0ca4e7b6cbf425488636ef942fe0d4c2bc';
|
String _$readerControllerHash() => r'89679c9f9542b8f3c7194190e08d0676d611e119';
|
||||||
|
|
||||||
final class ReaderControllerFamily extends $Family
|
final class ReaderControllerFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|
|
||||||
|
|
@ -1002,7 +1002,8 @@ class _MangaChapterPageGalleryState
|
||||||
);
|
);
|
||||||
|
|
||||||
_readerController.setMangaHistoryUpdate();
|
_readerController.setMangaHistoryUpdate();
|
||||||
await Future.delayed(const Duration(milliseconds: 1));
|
// Use post-frame callback instead of Future.delayed(1ms) timing hack
|
||||||
|
await Future(() {});
|
||||||
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
final fullScreenReader = ref.watch(fullScreenReaderStateProvider);
|
||||||
if (fullScreenReader) {
|
if (fullScreenReader) {
|
||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
|
|
@ -1050,9 +1051,7 @@ class _MangaChapterPageGalleryState
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
setState(() {
|
setState(() {
|
||||||
_readerController = ref.read(
|
_readerController = ref.read(
|
||||||
readerControllerProvider(
|
readerControllerProvider(chapter: pages[index].chapter!).notifier,
|
||||||
chapter: pages[index].chapter!,
|
|
||||||
).notifier,
|
|
||||||
);
|
);
|
||||||
chapter = pages[index].chapter!;
|
chapter = pages[index].chapter!;
|
||||||
final chapterUrlModel = pages[index].chapterUrlModel;
|
final chapterUrlModel = pages[index].chapterUrlModel;
|
||||||
|
|
@ -1165,7 +1164,8 @@ class _MangaChapterPageGalleryState
|
||||||
_scrollDirection = Axis.vertical;
|
_scrollDirection = Axis.vertical;
|
||||||
_isReverseHorizontal = false;
|
_isReverseHorizontal = false;
|
||||||
});
|
});
|
||||||
await Future.delayed(const Duration(milliseconds: 30));
|
// Wait for the next frame so the PageView rebuilds with new direction
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
|
|
||||||
_extendedController.jumpToPage(index);
|
_extendedController.jumpToPage(index);
|
||||||
}
|
}
|
||||||
|
|
@ -1180,7 +1180,8 @@ class _MangaChapterPageGalleryState
|
||||||
|
|
||||||
_scrollDirection = Axis.horizontal;
|
_scrollDirection = Axis.horizontal;
|
||||||
});
|
});
|
||||||
await Future.delayed(const Duration(milliseconds: 30));
|
// Wait for the next frame so the PageView rebuilds with new direction
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
|
|
||||||
_extendedController.jumpToPage(index);
|
_extendedController.jumpToPage(index);
|
||||||
}
|
}
|
||||||
|
|
@ -1189,7 +1190,8 @@ class _MangaChapterPageGalleryState
|
||||||
setState(() {
|
setState(() {
|
||||||
_isReverseHorizontal = false;
|
_isReverseHorizontal = false;
|
||||||
});
|
});
|
||||||
await Future.delayed(const Duration(milliseconds: 30));
|
// Wait for the next frame so the scroll view rebuilds
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
_itemScrollController.scrollTo(
|
_itemScrollController.scrollTo(
|
||||||
index: index,
|
index: index,
|
||||||
duration: const Duration(milliseconds: 1),
|
duration: const Duration(milliseconds: 1),
|
||||||
|
|
|
||||||
|
|
@ -129,7 +129,8 @@ class _ChapterListWidgetState extends State<ChapterListWidget> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _jumpTo() async {
|
Future<void> _jumpTo() async {
|
||||||
await Future.delayed(const Duration(milliseconds: 5));
|
// Wait for the scroll view to layout before jumping
|
||||||
|
await WidgetsBinding.instance.endOfFrame;
|
||||||
controller.jumpTo(
|
controller.jumpTo(
|
||||||
controller.position.maxScrollExtent /
|
controller.position.maxScrollExtent /
|
||||||
chapterList.length *
|
chapterList.length *
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ final class GetHtmlContentProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$getHtmlContentHash() => r'ef15133ac4066d556a03b42addf01be916e529bc';
|
String _$getHtmlContentHash() => r'03e421b7f7e821526c47f3b460fc9d866f56c9f6';
|
||||||
|
|
||||||
final class GetHtmlContentFamily extends $Family
|
final class GetHtmlContentFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<FutureOr<(String, EpubNovel?)>, Chapter> {
|
with $FunctionalFamilyOverride<FutureOr<(String, EpubNovel?)>, Chapter> {
|
||||||
|
|
|
||||||
|
|
@ -66,7 +66,7 @@ final class SourceBaseUrlProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$sourceBaseUrlHash() => r'ead3cca719e2530502d97613e3168e0031eecde7';
|
String _$sourceBaseUrlHash() => r'8b39ad1c4c8283700b2d16dfa3036acc766bb5d4';
|
||||||
|
|
||||||
final class SourceBaseUrlFamily extends $Family
|
final class SourceBaseUrlFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<String, Source> {
|
with $FunctionalFamilyOverride<String, Source> {
|
||||||
|
|
|
||||||
|
|
@ -65,7 +65,7 @@ final class SupportsLatestProvider extends $FunctionalProvider<bool, bool, bool>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$supportsLatestHash() => r'e2d9b73adde86f78f1ab1c97d91ea2d3a59dc78d';
|
String _$supportsLatestHash() => r'1fbe2d182136169b88af7ba44d83676f4ec52d9f';
|
||||||
|
|
||||||
final class SupportsLatestFamily extends $Family
|
final class SupportsLatestFamily extends $Family
|
||||||
with $FunctionalFamilyOverride<bool, Source> {
|
with $FunctionalFamilyOverride<bool, Source> {
|
||||||
|
|
|
||||||
|
|
@ -91,7 +91,7 @@ final class HeadersProvider
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$headersHash() => r'6ad2d5394456d7c054f1270a9f774329ccbb5dad';
|
String _$headersHash() => r'6d6fd92c9b4137f0c7189ed29a8730fecea6fc99';
|
||||||
|
|
||||||
final class HeadersFamily extends $Family
|
final class HeadersFamily extends $Family
|
||||||
with
|
with
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue