mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-03-11 17:25:32 +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 {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
// Wait for the widget tree to settle before updating fit
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
if (mounted) {
|
||||
_key.currentState?.update(
|
||||
fit: fit,
|
||||
|
|
|
|||
|
|
@ -48,7 +48,8 @@ class _SubtitlesWidgetSearchState extends ConsumerState<SubtitlesWidgetSearch> {
|
|||
}
|
||||
|
||||
Future<void> _init() async {
|
||||
await Future.delayed(const Duration(microseconds: 100));
|
||||
// Yield to microtask queue so initState completes before async work
|
||||
await Future(() {});
|
||||
try {
|
||||
titles = await fetchImdbTitles(query);
|
||||
} catch (e) {
|
||||
|
|
|
|||
|
|
@ -96,7 +96,8 @@ class _CodeEditorPageState extends ConsumerState<CodeEditorPage> {
|
|||
_logSubscription = _logStreamController.stream.listen((event) async {
|
||||
_addLog(event);
|
||||
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) {
|
||||
_scrollController.jumpTo(_scrollController.position.maxScrollExtent);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,7 +76,8 @@ class _GlobalSearchScreenState extends ConsumerState<GlobalSearchScreen> {
|
|||
setState(() {
|
||||
_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(() {
|
||||
_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 {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
// Wait for the widget tree to settle before loading detail
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
await ref.read(
|
||||
updateMangaDetailProvider(mangaId: widget.mangaId, isInit: true).future,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -373,7 +373,8 @@ class ChaptersListttState extends _$ChaptersListttState {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -958,7 +958,7 @@ final class ChaptersListttStateProvider
|
|||
}
|
||||
|
||||
String _$chaptersListttStateHash() =>
|
||||
r'f45ebd9a5b1fd86b279e263813098564830c2536';
|
||||
r'55c0093bb370d4d103129eeca67e652a0241f2c0';
|
||||
|
||||
abstract class _$ChaptersListttState extends $Notifier<List<Chapter>> {
|
||||
List<Chapter> build();
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ final class UpdateMangaDetailProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$updateMangaDetailHash() => r'37da5f23f30126d15cedfaf42087f9ce11c3fc26';
|
||||
String _$updateMangaDetailHash() => r'7071020d9d5dd6477875cc8fa0f226bd1d676620';
|
||||
|
||||
final class UpdateMangaDetailFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -92,7 +92,8 @@ class _MigrationScreenScreenState extends ConsumerState<MigrationScreen> {
|
|||
setState(() {
|
||||
_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(() {
|
||||
_query = value;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -38,7 +38,8 @@ class _TrackerWidgetSearchState extends ConsumerState<TrackerWidgetSearch> {
|
|||
late List<TrackSearch>? tracks = [];
|
||||
String? _errorMsg;
|
||||
Future<void> _init() async {
|
||||
await Future.delayed(const Duration(microseconds: 100));
|
||||
// Yield to microtask queue so initState completes before async work
|
||||
await Future(() {});
|
||||
try {
|
||||
tracks = await ref
|
||||
.read(
|
||||
|
|
|
|||
|
|
@ -40,7 +40,8 @@ class _TrackerWidgetState extends ConsumerState<TrackerWidget> {
|
|||
}
|
||||
|
||||
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
|
||||
.read(
|
||||
trackStateProvider(
|
||||
|
|
|
|||
|
|
@ -136,7 +136,7 @@ final class DownloadChapterProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$downloadChapterHash() => r'c503cef46aa7083316b023400f0aa470ae3a3bc4';
|
||||
String _$downloadChapterHash() => r'db235f856cf106c89d6124c361a51f2e312e9aa3';
|
||||
|
||||
final class DownloadChapterFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -147,7 +147,7 @@ final class ReaderControllerProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$readerControllerHash() => r'23eece0ca4e7b6cbf425488636ef942fe0d4c2bc';
|
||||
String _$readerControllerHash() => r'89679c9f9542b8f3c7194190e08d0676d611e119';
|
||||
|
||||
final class ReaderControllerFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
|
|
@ -1002,7 +1002,8 @@ class _MangaChapterPageGalleryState
|
|||
);
|
||||
|
||||
_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);
|
||||
if (fullScreenReader) {
|
||||
if (isDesktop) {
|
||||
|
|
@ -1050,9 +1051,7 @@ class _MangaChapterPageGalleryState
|
|||
if (mounted) {
|
||||
setState(() {
|
||||
_readerController = ref.read(
|
||||
readerControllerProvider(
|
||||
chapter: pages[index].chapter!,
|
||||
).notifier,
|
||||
readerControllerProvider(chapter: pages[index].chapter!).notifier,
|
||||
);
|
||||
chapter = pages[index].chapter!;
|
||||
final chapterUrlModel = pages[index].chapterUrlModel;
|
||||
|
|
@ -1165,7 +1164,8 @@ class _MangaChapterPageGalleryState
|
|||
_scrollDirection = Axis.vertical;
|
||||
_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);
|
||||
}
|
||||
|
|
@ -1180,7 +1180,8 @@ class _MangaChapterPageGalleryState
|
|||
|
||||
_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);
|
||||
}
|
||||
|
|
@ -1189,7 +1190,8 @@ class _MangaChapterPageGalleryState
|
|||
setState(() {
|
||||
_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(
|
||||
index: index,
|
||||
duration: const Duration(milliseconds: 1),
|
||||
|
|
|
|||
|
|
@ -129,7 +129,8 @@ class _ChapterListWidgetState extends State<ChapterListWidget> {
|
|||
}
|
||||
|
||||
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.position.maxScrollExtent /
|
||||
chapterList.length *
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ final class GetHtmlContentProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$getHtmlContentHash() => r'ef15133ac4066d556a03b42addf01be916e529bc';
|
||||
String _$getHtmlContentHash() => r'03e421b7f7e821526c47f3b460fc9d866f56c9f6';
|
||||
|
||||
final class GetHtmlContentFamily extends $Family
|
||||
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
|
||||
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
|
||||
with $FunctionalFamilyOverride<bool, Source> {
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ final class HeadersProvider
|
|||
}
|
||||
}
|
||||
|
||||
String _$headersHash() => r'6ad2d5394456d7c054f1270a9f774329ccbb5dad';
|
||||
String _$headersHash() => r'6d6fd92c9b4137f0c7189ed29a8730fecea6fc99';
|
||||
|
||||
final class HeadersFamily extends $Family
|
||||
with
|
||||
|
|
|
|||
Loading…
Reference in a new issue