diff --git a/lib/modules/browse/browse_screen.dart b/lib/modules/browse/browse_screen.dart index 9067e763..7485c2bf 100644 --- a/lib/modules/browse/browse_screen.dart +++ b/lib/modules/browse/browse_screen.dart @@ -12,6 +12,7 @@ import 'package:mangayomi/modules/browse/extension/extension_screen.dart'; import 'package:mangayomi/modules/browse/sources/sources_screen.dart'; import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart'; import 'package:mangayomi/services/fetch_sources_list.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; class BrowseScreen extends ConsumerStatefulWidget { const BrowseScreen({super.key}); @@ -20,19 +21,35 @@ class BrowseScreen extends ConsumerStatefulWidget { ConsumerState createState() => _BrowseScreenState(); } +enum BrowseTabKind { sources, extensions } + +class BrowseTab { + final ItemType type; + final BrowseTabKind kind; + + const BrowseTab(this.type, this.kind); +} + class _BrowseScreenState extends ConsumerState with TickerProviderStateMixin { late final hideItems = ref.read(hideItemsStateProvider); final _textEditingController = TextEditingController(); late TabController _tabBarController; - late final _tabList = [ - if (!hideItems.contains("/MangaLibrary")) 'manga', - if (!hideItems.contains("/AnimeLibrary")) 'anime', - if (!hideItems.contains("/NovelLibrary")) 'novel', - if (!hideItems.contains("/MangaLibrary")) 'mangaExtension', - if (!hideItems.contains("/AnimeLibrary")) 'animeExtension', - if (!hideItems.contains("/NovelLibrary")) 'novelExtension', + late final List _tabList = [ + if (!hideItems.contains("/MangaLibrary")) + BrowseTab(ItemType.manga, BrowseTabKind.sources), + if (!hideItems.contains("/AnimeLibrary")) + BrowseTab(ItemType.anime, BrowseTabKind.sources), + if (!hideItems.contains("/NovelLibrary")) + BrowseTab(ItemType.novel, BrowseTabKind.sources), + + if (!hideItems.contains("/MangaLibrary")) + BrowseTab(ItemType.manga, BrowseTabKind.extensions), + if (!hideItems.contains("/AnimeLibrary")) + BrowseTab(ItemType.anime, BrowseTabKind.extensions), + if (!hideItems.contains("/NovelLibrary")) + BrowseTab(ItemType.novel, BrowseTabKind.extensions), ]; @override @@ -65,11 +82,8 @@ class _BrowseScreenState extends ConsumerState if (_tabList.isEmpty) { return SizedBox.shrink(); } - final containsExtensionTab = [ - "mangaExtension", - "animeExtension", - "novelExtension", - ].any((element) => _tabList[_tabBarController.index] == element); + final currentTab = _tabList[_tabBarController.index]; + final isExtensionTab = currentTab.kind == BrowseTabKind.extensions; final l10n = l10nLocalizations(context)!; return DefaultTabController( @@ -102,9 +116,7 @@ class _BrowseScreenState extends ConsumerState ) : Row( children: [ - if (_tabBarController.index == 3 || - _tabBarController.index == 4 || - _tabBarController.index == 5) + if (isExtensionTab) IconButton( onPressed: () { context.push('/createExtension'); @@ -117,26 +129,19 @@ class _BrowseScreenState extends ConsumerState IconButton( splashRadius: 20, onPressed: () { - if (containsExtensionTab) { + if (isExtensionTab) { setState(() { _isSearch = true; }); } else { context.push( '/globalSearch', - extra: ( - null, - switch (_tabList[_tabBarController.index]) { - "manga" => ItemType.manga, - "anime" => ItemType.anime, - _ => ItemType.novel, - }, - ), + extra: (null, currentTab.type), ); } }, icon: Icon( - !containsExtensionTab + !isExtensionTab ? Icons.travel_explore_rounded : Icons.search_rounded, color: Theme.of(context).hintColor, @@ -148,16 +153,12 @@ class _BrowseScreenState extends ConsumerState splashRadius: 20, onPressed: () { context.push( - containsExtensionTab ? '/ExtensionLang' : '/sourceFilter', - extra: switch (_tabList[_tabBarController.index]) { - "manga" || "mangaExtension" => ItemType.manga, - "anime" || "animeExtension" => ItemType.anime, - _ => ItemType.novel, - }, + isExtensionTab ? '/ExtensionLang' : '/sourceFilter', + extra: currentTab.type, ); }, icon: Icon( - !containsExtensionTab + !isExtensionTab ? Icons.filter_list_sharp : Icons.translate_rounded, color: Theme.of(context).hintColor, @@ -168,86 +169,44 @@ class _BrowseScreenState extends ConsumerState indicatorSize: TabBarIndicatorSize.label, isScrollable: true, controller: _tabBarController, - tabs: [ - if (!hideItems.contains("/MangaLibrary")) - Tab(text: l10n.manga_sources), - if (!hideItems.contains("/AnimeLibrary")) - Tab(text: l10n.anime_sources), - if (!hideItems.contains("/NovelLibrary")) - Tab(text: l10n.novel_sources), - if (!hideItems.contains("/MangaLibrary")) - Tab( - child: Row( - children: [ - Text(l10n.manga_extensions), + tabs: _tabList.map((tab) { + final type = tab.type; + final isExt = tab.kind == BrowseTabKind.extensions; + + return Tab( + child: Row( + children: [ + Text( + isExt + ? type.localizedExtensions(l10n) + : type.localizedSources(l10n), + ), + if (isExt) ...[ const SizedBox(width: 8), - _extensionUpdateNumbers(ref, ItemType.manga), + _extensionUpdateNumbers(ref, type), ], - ), + ], ), - if (!hideItems.contains("/AnimeLibrary")) - Tab( - child: Row( - children: [ - Text(l10n.anime_extensions), - const SizedBox(width: 8), - _extensionUpdateNumbers(ref, ItemType.anime), - ], - ), - ), - if (!hideItems.contains("/NovelLibrary")) - Tab( - child: Row( - children: [ - Text(l10n.novel_extensions), - const SizedBox(width: 8), - _extensionUpdateNumbers(ref, ItemType.novel), - ], - ), - ), - ], + ); + }).toList(), ), ), body: TabBarView( controller: _tabBarController, - children: [ - if (!hideItems.contains("/MangaLibrary")) - SourcesScreen( - itemType: ItemType.manga, - tabIndex: (index) { - _tabBarController.animateTo(index); - }, - ), - if (!hideItems.contains("/AnimeLibrary")) - SourcesScreen( - itemType: ItemType.anime, - tabIndex: (index) { - _tabBarController.animateTo(index); - }, - ), - if (!hideItems.contains("/NovelLibrary")) - SourcesScreen( - itemType: ItemType.novel, - tabIndex: (index) { - _tabBarController.animateTo(index); - }, - ), - if (!hideItems.contains("/MangaLibrary")) - ExtensionScreen( + children: _tabList.map((tab) { + if (tab.kind == BrowseTabKind.sources) { + return SourcesScreen( + itemType: tab.type, + tabs: _tabList, + tabIndex: (index) => _tabBarController.animateTo(index), + ); + } else { + return ExtensionScreen( query: _textEditingController.text, - itemType: ItemType.manga, - ), - if (!hideItems.contains("/AnimeLibrary")) - ExtensionScreen( - query: _textEditingController.text, - itemType: ItemType.anime, - ), - if (!hideItems.contains("/NovelLibrary")) - ExtensionScreen( - query: _textEditingController.text, - itemType: ItemType.novel, - ), - ], + itemType: tab.type, + ); + } + }).toList(), ), ), ); diff --git a/lib/modules/browse/sources/sources_screen.dart b/lib/modules/browse/sources/sources_screen.dart index fb0881c1..098f9846 100644 --- a/lib/modules/browse/sources/sources_screen.dart +++ b/lib/modules/browse/sources/sources_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mangayomi/modules/browse/browse_screen.dart'; import 'package:mangayomi/modules/widgets/custom_sliver_grouped_list_view.dart'; import 'package:isar_community/isar.dart'; import 'package:mangayomi/main.dart'; @@ -11,10 +12,12 @@ import 'package:mangayomi/utils/language.dart'; class SourcesScreen extends ConsumerStatefulWidget { final Function(int) tabIndex; + final List tabs; final ItemType itemType; const SourcesScreen({ required this.tabIndex, required this.itemType, + required this.tabs, super.key, }); @@ -62,13 +65,17 @@ class _SourcesScreenState extends ConsumerState { Padding( padding: const EdgeInsets.all(8.0), child: ElevatedButton.icon( - onPressed: () => widget.tabIndex( - widget.itemType == ItemType.manga - ? 3 - : widget.itemType == ItemType.anime - ? 4 - : 5, - ), + onPressed: () { + final extensionIndex = widget.tabs.indexWhere( + (t) => + t.type == widget.itemType && + t.kind == BrowseTabKind.extensions, + ); + + if (extensionIndex != -1) { + widget.tabIndex(extensionIndex); + } + }, icon: const Icon(Icons.extension_rounded), label: Text(context.l10n.show_extensions), ), diff --git a/lib/modules/browse/sources/widgets/source_list_tile.dart b/lib/modules/browse/sources/widgets/source_list_tile.dart index 9abb0729..695a456d 100644 --- a/lib/modules/browse/sources/widgets/source_list_tile.dart +++ b/lib/modules/browse/sources/widgets/source_list_tile.dart @@ -8,6 +8,7 @@ import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/utils/cached_network.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; import 'package:mangayomi/utils/language.dart'; class SourceListTile extends StatelessWidget { @@ -81,11 +82,7 @@ class SourceListTile extends StatelessWidget { title: Text( !isLocal ? source.name! - : "${context.l10n.local_source} ${source.itemType == ItemType.manga - ? context.l10n.manga - : source.itemType == ItemType.anime - ? context.l10n.anime - : context.l10n.novel}", + : "${context.l10n.local_source} ${source.itemType.localized(context.l10n)}", ), trailing: SizedBox( width: 150, diff --git a/lib/modules/calendar/calendar_screen.dart b/lib/modules/calendar/calendar_screen.dart index 91a7fe5b..b6347864 100644 --- a/lib/modules/calendar/calendar_screen.dart +++ b/lib/modules/calendar/calendar_screen.dart @@ -7,6 +7,7 @@ import 'package:isar_community/isar.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/modules/calendar/providers/calendar_provider.dart'; +import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart'; import 'package:mangayomi/modules/widgets/custom_sliver_grouped_list_view.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart'; @@ -15,6 +16,8 @@ import 'package:mangayomi/utils/constant.dart'; import 'package:mangayomi/utils/date.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; import 'package:mangayomi/utils/headers.dart'; +import 'package:mangayomi/utils/item_type_filters.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; import 'package:table_calendar/table_calendar.dart'; class CalendarScreen extends ConsumerStatefulWidget { @@ -35,11 +38,19 @@ class _CalendarScreenState extends ConsumerState { DateTime? _selectedDay; DateTime? _rangeStart; DateTime? _rangeEnd; - late ItemType? itemType = widget.itemType ?? ItemType.manga; + late ItemType? itemType; + late List _visibleTypes; @override void initState() { super.initState(); + _visibleTypes = hiddenItemTypes(ref.read(hideItemsStateProvider)); + final initialItemType = widget.itemType ?? ItemType.manga; + if (_visibleTypes.contains(initialItemType)) { + itemType = initialItemType; + } else { + itemType = _visibleTypes.isNotEmpty ? _visibleTypes.first : null; + } _selectedDay = _focusedDay; _selectedEntries = ValueNotifier([]); } @@ -69,31 +80,7 @@ class _CalendarScreenState extends ConsumerState { SliverToBoxAdapter( child: Column( children: [ - ListTile( - title: Padding( - padding: const EdgeInsets.symmetric(vertical: 3), - child: Row( - children: [ - Icon( - Icons.warning_amber_outlined, - color: context.secondaryColor, - ), - const SizedBox(width: 10), - Flexible( - child: Text( - l10n.calendar_info, - softWrap: true, - overflow: TextOverflow.clip, - style: TextStyle( - fontSize: 13, - color: context.secondaryColor, - ), - ), - ), - ], - ), - ), - ), + _buildWarningTile(context), Padding( padding: const EdgeInsets.symmetric(horizontal: 15), child: Row( @@ -107,29 +94,15 @@ class _CalendarScreenState extends ConsumerState { borderRadius: BorderRadius.circular(50), ), ), - segments: [ - ButtonSegment( - value: ItemType.manga.index, + segments: _visibleTypes.map((type) { + return ButtonSegment( + value: type.index, label: Padding( padding: const EdgeInsets.all(12), - child: Text(l10n.manga), + child: Text(type.localized(l10n)), ), - ), - ButtonSegment( - value: ItemType.anime.index, - label: Padding( - padding: const EdgeInsets.all(12), - child: Text(l10n.anime), - ), - ), - ButtonSegment( - value: ItemType.novel.index, - label: Padding( - padding: const EdgeInsets.all(12), - child: Text(l10n.novel), - ), - ), - ], + ); + }).toList(), selected: {itemType?.index}, onSelectionChanged: (newSelection) { if (newSelection.isNotEmpty && @@ -145,40 +118,7 @@ class _CalendarScreenState extends ConsumerState { ], ), ), - TableCalendar( - firstDay: firstDay, - lastDay: lastDay, - focusedDay: _focusedDay, - locale: locale.toLanguageTag(), - selectedDayPredicate: (day) => - isSameDay(_selectedDay, day), - rangeStartDay: _rangeStart, - rangeEndDay: _rangeEnd, - calendarFormat: _calendarFormat, - rangeSelectionMode: _rangeSelectionMode, - eventLoader: (day) => _getEntriesForDay(day, data), - startingDayOfWeek: StartingDayOfWeek.monday, - calendarStyle: CalendarStyle( - outsideDaysVisible: true, - weekendTextStyle: TextStyle( - color: context.primaryColor, - ), - ), - onDaySelected: (selectedDay, focusedDay) => - _onDaySelected(selectedDay, focusedDay, data), - onRangeSelected: (start, end, focusedDay) => - _onRangeSelected(start, end, focusedDay, data), - onFormatChanged: (format) { - if (_calendarFormat != format) { - setState(() { - _calendarFormat = format; - }); - } - }, - onPageChanged: (focusedDay) { - _focusedDay = focusedDay; - }, - ), + _buildCalendar(data, locale), const SizedBox(height: 15), ], ), @@ -241,8 +181,64 @@ class _CalendarScreenState extends ConsumerState { ); } + Widget _buildWarningTile(BuildContext context) { + return ListTile( + title: Padding( + padding: const EdgeInsets.symmetric(vertical: 3), + child: Row( + children: [ + Icon(Icons.warning_amber_outlined, color: context.secondaryColor), + const SizedBox(width: 10), + Flexible( + child: Text( + context.l10n.calendar_info, + softWrap: true, + overflow: TextOverflow.clip, + style: TextStyle(fontSize: 13, color: context.secondaryColor), + ), + ), + ], + ), + ), + ); + } + + Widget _buildCalendar(List data, Locale locale) { + return TableCalendar( + firstDay: firstDay, + lastDay: lastDay, + focusedDay: _focusedDay, + locale: locale.toLanguageTag(), + selectedDayPredicate: (day) => isSameDay(_selectedDay, day), + rangeStartDay: _rangeStart, + rangeEndDay: _rangeEnd, + calendarFormat: _calendarFormat, + rangeSelectionMode: _rangeSelectionMode, + eventLoader: (day) => _getEntriesForDay(day, data), + startingDayOfWeek: StartingDayOfWeek.monday, + calendarStyle: CalendarStyle( + outsideDaysVisible: true, + weekendTextStyle: TextStyle(color: context.primaryColor), + ), + onDaySelected: (selectedDay, focusedDay) => + _onDaySelected(selectedDay, focusedDay, data), + onRangeSelected: (start, end, focusedDay) => + _onRangeSelected(start, end, focusedDay, data), + onFormatChanged: (format) { + if (_calendarFormat != format) { + setState(() => _calendarFormat = format); + } + }, + onPageChanged: (focusedDay) => _focusedDay = focusedDay, + ); + } + + final Map> _dayCache = {}; + List _getEntriesForDay(DateTime day, List data) { - return data.where((e) { + final key = "${day.year}-${day.month}-${day.day}"; + if (_dayCache.containsKey(key)) return _dayCache[key]!; + final result = data.where((e) { final lastChapter = e.chapters .filter() .sortByDateUploadDesc() @@ -252,10 +248,12 @@ class _CalendarScreenState extends ConsumerState { ? DateTime.fromMillisecondsSinceEpoch(lastDate) : DateTime.now(); final temp = start.add(Duration(days: e.smartUpdateDays!)); - final predictedDay = "${temp.year}-${temp.month}-${temp.day}"; - final selectedDay = "${day.year}-${day.month}-${day.day}"; - return predictedDay == selectedDay; + return temp.year == day.year && + temp.month == day.month && + temp.day == day.day; }).toList(); + _dayCache[key] = result; + return result; } List _getEntriesForRange( diff --git a/lib/modules/history/history_screen.dart b/lib/modules/history/history_screen.dart index 2a4d8151..2d90e651 100644 --- a/lib/modules/history/history_screen.dart +++ b/lib/modules/history/history_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:mangayomi/l10n/generated/app_localizations.dart'; +import 'package:mangayomi/modules/widgets/base_library_tab_screen.dart'; import 'package:mangayomi/modules/widgets/custom_sliver_grouped_list_view.dart'; import 'package:isar_community/isar.dart'; @@ -12,7 +13,6 @@ import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/modules/history/providers/isar_providers.dart'; -import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/utils/cached_network.dart'; @@ -20,7 +20,6 @@ import 'package:mangayomi/utils/constant.dart'; import 'package:mangayomi/utils/date.dart'; import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:mangayomi/utils/headers.dart'; -import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart'; import 'package:mangayomi/modules/widgets/error_text.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart'; @@ -31,175 +30,61 @@ class HistoryScreen extends ConsumerStatefulWidget { ConsumerState createState() => _HistoryScreenState(); } -class _HistoryScreenState extends ConsumerState - with TickerProviderStateMixin { - final _textEditingController = TextEditingController(); - late TabController _tabBarController; +class _HistoryScreenState extends BaseLibraryTabScreenState { + @override + String get title => l10nLocalizations(context)!.history; - void tabListener() { - setState(() { - _textEditingController.clear(); - _isSearch = false; - }); + @override + Widget buildTab(ItemType type) { + return HistoryTab(itemType: type, query: textEditingController.text); } @override - void initState() { - super.initState(); - final hideItems = ref.read(hideItemsStateProvider); - final tabCount = [ - if (!hideItems.contains("/MangaLibrary")) "/MangaLibrary", - if (!hideItems.contains("/AnimeLibrary")) "/AnimeLibrary", - if (!hideItems.contains("/NovelLibrary")) "/NovelLibrary", - ].length; - _tabBarController = TabController(length: tabCount, vsync: this); - _tabBarController.addListener(tabListener); - } - - @override - void dispose() { - _tabBarController.dispose(); - _textEditingController.dispose(); - super.dispose(); - } - - bool _isSearch = false; - @override - Widget build(BuildContext context) { - final hideItems = ref.watch(hideItemsStateProvider); + List buildExtraActions(BuildContext context) { final l10n = l10nLocalizations(context)!; - return Scaffold( - appBar: AppBar( - elevation: 0, - backgroundColor: Colors.transparent, - title: _isSearch - ? null - : Text( - l10n.history, - style: TextStyle(color: Theme.of(context).hintColor), - ), - actions: [ - _isSearch - ? SeachFormTextField( - onChanged: (value) { - setState(() {}); - }, - onSuffixPressed: () { - _textEditingController.clear(); - setState(() {}); - }, - onPressed: () { - setState(() { - _isSearch = false; - }); - _textEditingController.clear(); - }, - controller: _textEditingController, - ) - : IconButton( - splashRadius: 20, - onPressed: () { - setState(() { - _isSearch = true; - }); - }, - icon: Icon(Icons.search, color: Theme.of(context).hintColor), - ), - IconButton( - splashRadius: 20, - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(l10n.remove_everything), - content: Text(l10n.remove_everything_msg), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text(l10n.cancel), - ), - const SizedBox(width: 15), - TextButton( - onPressed: () async { - if (mounted) Navigator.pop(context); - await _clearHistory(hideItems); - }, - child: Text(l10n.ok), - ), - ], - ), - ], - ); - }, - ); - }, - icon: Icon( - Icons.delete_sweep_outlined, - color: Theme.of(context).hintColor, - ), - ), - ], - bottom: TabBar( - indicatorSize: TabBarIndicatorSize.tab, - controller: _tabBarController, - tabs: [ - if (!hideItems.contains("/MangaLibrary")) Tab(text: l10n.manga), - if (!hideItems.contains("/AnimeLibrary")) Tab(text: l10n.anime), - if (!hideItems.contains("/NovelLibrary")) Tab(text: l10n.novel), - ], + + return [ + IconButton( + splashRadius: 20, + icon: Icon( + Icons.delete_sweep_outlined, + color: Theme.of(context).hintColor, ), + onPressed: () { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.remove_everything), + content: Text(l10n.remove_everything_msg), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () async { + Navigator.of(dialogContext).pop(); + await _clearHistory(); + }, + child: Text(l10n.ok), + ), + ], + ), + ); + }, ), - body: TabBarView( - controller: _tabBarController, - children: [ - if (!hideItems.contains("/MangaLibrary")) - HistoryTab( - itemType: ItemType.manga, - query: _textEditingController.text, - ), - if (!hideItems.contains("/AnimeLibrary")) - HistoryTab( - itemType: ItemType.anime, - query: _textEditingController.text, - ), - if (!hideItems.contains("/NovelLibrary")) - HistoryTab( - itemType: ItemType.novel, - query: _textEditingController.text, - ), - ], - ), - ); + ]; } - Future _clearHistory(List hideItems) async { + Future _clearHistory() async { List histories = await isar.historys .filter() .idIsNotNull() - .chapter( - (q) => - q.manga((q) => q.itemTypeEqualTo(getCurrentItemType(hideItems))), - ) + .chapter((q) => q.manga((q) => q.itemTypeEqualTo(getCurrentItemType()))) .findAll(); final List idsToDelete = histories.map((h) => h.id!).toList(); await isar.writeTxn(() => isar.historys.deleteAll(idsToDelete)); } - - ItemType getCurrentItemType(List hideItems) { - return _tabBarController.index == 0 && !hideItems.contains("/MangaLibrary") - ? ItemType.manga - : _tabBarController.index == - 1 - (hideItems.contains("/MangaLibrary") ? 1 : 0) && - !hideItems.contains("/AnimeLibrary") - ? ItemType.anime - : ItemType.novel; - } } class HistoryTab extends ConsumerStatefulWidget { diff --git a/lib/modules/library/library_screen.dart b/lib/modules/library/library_screen.dart index 553c6e65..8b7c8cab 100644 --- a/lib/modules/library/library_screen.dart +++ b/lib/modules/library/library_screen.dart @@ -2,12 +2,10 @@ import 'dart:io'; import 'dart:math'; -import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:isar_community/isar.dart'; -import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/changed.dart'; import 'package:mangayomi/models/chapter.dart'; @@ -19,10 +17,8 @@ import 'package:mangayomi/models/update.dart'; import 'package:mangayomi/modules/library/providers/add_torrent.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/providers/update_manga_detail_providers.dart'; import 'package:mangayomi/modules/more/categories/providers/isar_providers.dart'; import 'package:mangayomi/modules/more/providers/downloaded_only_state_provider.dart'; -import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart'; import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/modules/widgets/bottom_select_bar.dart'; import 'package:mangayomi/modules/widgets/category_selection_dialog.dart'; @@ -30,6 +26,7 @@ import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart'; import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/providers/storage_provider.dart'; +import 'package:mangayomi/services/library_updater.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; import 'package:mangayomi/modules/library/providers/isar_providers.dart'; import 'package:mangayomi/modules/library/providers/library_state_provider.dart'; @@ -42,6 +39,7 @@ import 'package:mangayomi/modules/widgets/error_text.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart'; import 'package:mangayomi/utils/extensions/string_extensions.dart'; import 'package:mangayomi/utils/global_style.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; import 'package:path/path.dart' as p; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -82,53 +80,6 @@ class _LibraryScreenState extends ConsumerState super.dispose(); } - Future _updateLibrary(List mangaList) async { - bool isDark = ref.read(themeModeStateProvider); - botToast( - context.l10n.updating_library("0", "0", "0"), - fontSize: 13, - second: 30, - alignY: !context.isTablet ? 0.85 : 1, - themeDark: isDark, - ); - int numbers = 0; - int failed = 0; - for (var manga in mangaList) { - try { - await ref.read( - updateMangaDetailProvider( - mangaId: manga.id, - isInit: false, - showToast: false, - ).future, - ); - } catch (_) { - failed++; - } - numbers++; - if (mounted) { - botToast( - context.l10n.updating_library(numbers, failed, mangaList.length), - fontSize: 13, - second: 10, - alignY: !context.isTablet ? 0.85 : 1, - animationDuration: 0, - dismissDirections: [DismissDirection.none], - onlyOne: false, - themeDark: isDark, - ); - } - } - await Future.doWhile(() async { - await Future.delayed(const Duration(seconds: 1)); - if (mangaList.length == numbers) { - return false; - } - return true; - }); - BotToast.cleanAll(); - } - @override Widget build(BuildContext context) { final settingsStream = ref.watch(getSettingsStreamProvider); @@ -776,7 +727,12 @@ class _LibraryScreenState extends ConsumerState final entriesManga = reverse ? entries.reversed.toList() : entries; return RefreshIndicator( onRefresh: () async { - await _updateLibrary(data); + await updateLibrary( + ref: ref, + context: context, + mangaList: data, + itemType: widget.itemType, + ); }, child: displayType == DisplayType.list ? LibraryListViewWidget( @@ -867,7 +823,12 @@ class _LibraryScreenState extends ConsumerState final entriesManga = reverse ? entries.reversed.toList() : entries; return RefreshIndicator( onRefresh: () async { - await _updateLibrary(data); + await updateLibrary( + ref: ref, + context: context, + mangaList: data, + itemType: widget.itemType, + ); }, child: displayType == DisplayType.list ? LibraryListViewWidget( @@ -1901,11 +1862,7 @@ class _LibraryScreenState extends ConsumerState : Row( children: [ Text( - widget.itemType == ItemType.manga - ? l10n.manga - : widget.itemType == ItemType.anime - ? l10n.anime - : l10n.novel, + widget.itemType.localized(l10n), style: TextStyle(color: Theme.of(context).hintColor), ), const SizedBox(width: 10), @@ -2013,7 +1970,12 @@ class _LibraryScreenState extends ConsumerState onSelected: (value) { if (value == 0) { manga.whenData((value) { - _updateLibrary(value); + updateLibrary( + ref: ref, + context: context, + mangaList: value, + itemType: widget.itemType, + ); }); } else if (value == 1) { manga.whenData((value) { diff --git a/lib/modules/manga/home/manga_home_screen.dart b/lib/modules/manga/home/manga_home_screen.dart index 08179136..f0854859 100644 --- a/lib/modules/manga/home/manga_home_screen.dart +++ b/lib/modules/manga/home/manga_home_screen.dart @@ -25,6 +25,7 @@ import 'package:mangayomi/modules/manga/home/widget/mangas_card_selector.dart'; import 'package:mangayomi/modules/widgets/gridview_widget.dart'; import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart'; import 'package:mangayomi/utils/global_style.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; import 'package:marquee/marquee.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; @@ -167,11 +168,7 @@ class _MangaHomeScreenState extends ConsumerState { Text( !isLocal ? "${source.name}" - : "${context.l10n.local_source} ${source.itemType == ItemType.manga - ? context.l10n.manga - : source.itemType == ItemType.anime - ? context.l10n.anime - : context.l10n.novel}", + : "${context.l10n.local_source} ${source.itemType.localized(context.l10n)}", ), source.notes != null && source.notes!.isNotEmpty ? SizedBox( diff --git a/lib/modules/more/categories/categories_screen.dart b/lib/modules/more/categories/categories_screen.dart index a6999d71..90806740 100644 --- a/lib/modules/more/categories/categories_screen.dart +++ b/lib/modules/more/categories/categories_screen.dart @@ -11,6 +11,8 @@ import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_pr 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/utils/item_type_filters.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; class CategoriesScreen extends ConsumerStatefulWidget { @@ -24,17 +26,15 @@ class CategoriesScreen extends ConsumerStatefulWidget { class _CategoriesScreenState extends ConsumerState with TickerProviderStateMixin { late TabController _tabBarController; - late final List _tabList; + late final List _visibleTabTypes; @override void initState() { super.initState(); - final hideItems = ref.read(hideItemsStateProvider); - _tabList = [ - if (!hideItems.contains("/MangaLibrary")) "/MangaLibrary", - if (!hideItems.contains("/AnimeLibrary")) "/AnimeLibrary", - if (!hideItems.contains("/NovelLibrary")) "/NovelLibrary", - ]; - _tabBarController = TabController(length: _tabList.length, vsync: this); + _visibleTabTypes = hiddenItemTypes(ref.read(hideItemsStateProvider)); + _tabBarController = TabController( + length: _visibleTabTypes.length, + vsync: this, + ); _tabBarController.animateTo(widget.data.$2); } @@ -46,7 +46,7 @@ class _CategoriesScreenState extends ConsumerState @override Widget build(BuildContext context) { - if (_tabList.isEmpty) { + if (_visibleTabTypes.isEmpty) { return Scaffold( appBar: AppBar(title: Text(context.l10n.categories)), body: Center(child: Text("EMPTY\nMPTY\nMTY\nMT\n\n")), @@ -55,7 +55,7 @@ class _CategoriesScreenState extends ConsumerState final l10n = l10nLocalizations(context)!; return DefaultTabController( animationDuration: Duration.zero, - length: _tabList.length, + length: _visibleTabTypes.length, child: Scaffold( appBar: AppBar( elevation: 0, @@ -67,23 +67,15 @@ class _CategoriesScreenState extends ConsumerState bottom: TabBar( indicatorSize: TabBarIndicatorSize.label, controller: _tabBarController, - tabs: _tabList.map((route) { - if (route == "/MangaLibrary") return Tab(text: l10n.manga); - if (route == "/AnimeLibrary") return Tab(text: l10n.anime); - return Tab(text: l10n.novel); + tabs: _visibleTabTypes.map((type) { + return Tab(text: type.localized(l10n)); }).toList(), ), ), body: TabBarView( controller: _tabBarController, - children: _tabList.map((route) { - if (route == "/MangaLibrary") { - return CategoriesTab(itemType: ItemType.manga); - } - if (route == "/AnimeLibrary") { - return CategoriesTab(itemType: ItemType.anime); - } - return CategoriesTab(itemType: ItemType.novel); + children: _visibleTabTypes.map((type) { + return CategoriesTab(itemType: type); }).toList(), ), ), @@ -101,15 +93,25 @@ class CategoriesTab extends ConsumerStatefulWidget { class _CategoriesTabState extends ConsumerState { List _entries = []; - void _updateCategoriesOrder(List categories) { - isar.writeTxnSync(() { - isar.categorys.clearSync(); - isar.categorys.putAllSync(categories); - final cats = isar.categorys.filter().posIsNull().findAllSync(); - for (var category in cats) { - isar.categorys.putSync(category..pos = category.id); - } - }); + + /// Moves a category from `index` to `newIndex` in the list, + /// swaps their positions in memory, and persists the change in Isar. + Future _moveCategory(int index, int newIndex) async { + // Prevent invalid moves (out of bounds) + if (newIndex < 0 || newIndex >= _entries.length) return; + // Grab the two category objects involved in the swap + final a = _entries[index]; + final b = _entries[newIndex]; + // Swap their positions inside the in‑memory list + _entries[newIndex] = a; + _entries[index] = b; + // Swap their persisted `pos` values so ordering is saved correctly + final temp = a.pos; + a.pos = b.pos; + b.pos = temp; + // Persist both updated objects in a single Isar transaction + await isar.writeTxn(() async => isar.categorys.putAll([a, b])); + setState(() {}); // Trigger a UI rebuild to reflect the updated order } @override @@ -189,17 +191,7 @@ class _CategoriesTabState extends ConsumerState { ), onPressed: index > 0 ? () { - final item = _entries[index - 1]; - _entries.removeAt(index); - _entries.removeAt(index - 1); - int? currentPos = category.pos; - int? pos = item.pos; - setState(() {}); - _updateCategoriesOrder([ - ..._entries, - category..pos = pos, - item..pos = currentPos, - ]); + _moveCategory(index, index - 1); } : null, ), @@ -209,17 +201,7 @@ class _CategoriesTabState extends ConsumerState { ), onPressed: index < _entries.length - 1 ? () { - final item = _entries[index + 1]; - _entries.removeAt(index + 1); - _entries.removeAt(index); - int? currentPos = category.pos; - int? pos = item.pos; - setState(() {}); - _updateCategoriesOrder([ - ..._entries, - category..pos = pos, - item..pos = currentPos, - ]); + _moveCategory(index, index + 1); } : null, ), @@ -239,12 +221,12 @@ class _CategoriesTabState extends ConsumerState { ), SizedBox(width: 10), IconButton( - onPressed: () { - isar.writeTxnSync(() async { + onPressed: () async { + await isar.writeTxn(() async { category.hide = !(category.hide ?? false); category.updatedAt = DateTime.now().millisecondsSinceEpoch; - isar.categorys.putSync(category); + isar.categorys.put(category); }); }, icon: Icon( diff --git a/lib/modules/more/data_and_storage/providers/backup.dart b/lib/modules/more/data_and_storage/providers/backup.dart index 666ff693..891705d3 100644 --- a/lib/modules/more/data_and_storage/providers/backup.dart +++ b/lib/modules/more/data_and_storage/providers/backup.dart @@ -175,17 +175,13 @@ Future doBackUp( alignment: Alignment.topLeft, child: ElevatedButton( onPressed: () { - final box = () { - try { - return context.findRenderObject() as RenderBox?; - } catch (e) { - return null; - } - }(); + final RenderBox? box = + context.findRenderObject() as RenderBox?; SharePlus.instance.share( ShareParams( files: [XFile(p.join(path, "$name.backup"))], - text: "$name.backup", + subject: "$name.backup", + title: "Share Mangayomi backup file", sharePositionOrigin: box == null ? null : box.localToGlobal(Offset.zero) & box.size, diff --git a/lib/modules/more/statistics/statistics_screen.dart b/lib/modules/more/statistics/statistics_screen.dart index e42c8ae7..80d383cb 100644 --- a/lib/modules/more/statistics/statistics_screen.dart +++ b/lib/modules/more/statistics/statistics_screen.dart @@ -5,6 +5,8 @@ import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_pr import 'package:mangayomi/modules/more/statistics/statistics_provider.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; +import 'package:mangayomi/utils/item_type_filters.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; class StatisticsScreen extends ConsumerStatefulWidget { const StatisticsScreen({super.key}); @@ -15,20 +17,17 @@ class StatisticsScreen extends ConsumerStatefulWidget { class _StatisticsScreenState extends ConsumerState with SingleTickerProviderStateMixin { - late final List hideItems; late TabController _tabController; - late final List _tabList; + late final List _visibleTabTypes; @override void initState() { super.initState(); - hideItems = ref.read(hideItemsStateProvider); - _tabList = [ - if (!hideItems.contains("/MangaLibrary")) "/MangaLibrary", - if (!hideItems.contains("/AnimeLibrary")) "/AnimeLibrary", - if (!hideItems.contains("/NovelLibrary")) "/NovelLibrary", - ]; - _tabController = TabController(length: _tabList.length, vsync: this); + _visibleTabTypes = hiddenItemTypes(ref.read(hideItemsStateProvider)); + _tabController = TabController( + length: _visibleTabTypes.length, + vsync: this, + ); } @override @@ -39,7 +38,7 @@ class _StatisticsScreenState extends ConsumerState @override Widget build(BuildContext context) { - if (_tabList.isEmpty) { + if (_visibleTabTypes.isEmpty) { return Scaffold( appBar: AppBar(title: Text(context.l10n.statistics)), body: Center(child: Text("EMPTY\nMPTY\nMTY\nMT\n\n")), @@ -51,23 +50,15 @@ class _StatisticsScreenState extends ConsumerState title: Text(l10n.statistics), bottom: TabBar( controller: _tabController, - tabs: _tabList.map((route) { - if (route == "/MangaLibrary") return Tab(text: l10n.manga); - if (route == "/AnimeLibrary") return Tab(text: l10n.anime); - return Tab(text: l10n.novel); + tabs: _visibleTabTypes.map((type) { + return Tab(text: type.localized(l10n)); }).toList(), ), ), body: TabBarView( controller: _tabController, - children: _tabList.map((route) { - if (route == "/MangaLibrary") { - return _buildStatisticsTab(itemType: ItemType.manga); - } - if (route == "/AnimeLibrary") { - return _buildStatisticsTab(itemType: ItemType.anime); - } - return _buildStatisticsTab(itemType: ItemType.novel); + children: _visibleTabTypes.map((type) { + return _buildStatisticsTab(itemType: type); }).toList(), ), ); @@ -77,19 +68,11 @@ class _StatisticsScreenState extends ConsumerState final l10n = context.l10n; final stats = ref.watch(getStatisticsProvider(itemType: itemType)); - final title = switch (itemType) { - ItemType.manga => l10n.manga, - ItemType.anime => l10n.anime, - _ => l10n.novel, - }; - final chapterLabel = switch (itemType) { - ItemType.manga => l10n.chapters, ItemType.anime => l10n.episodes, _ => l10n.chapters, }; final unreadLabel = switch (itemType) { - ItemType.manga => l10n.unread, ItemType.anime => l10n.unwatched, _ => l10n.unread, }; @@ -132,7 +115,7 @@ class _StatisticsScreenState extends ConsumerState downloadedItems: downloadedItems, averageChapters: averageChapters.toDouble(), readPercentage: readPercentage.toDouble(), - title: title, + title: itemType.localized(l10n), context: context, unreadLabel: unreadLabel, ), diff --git a/lib/modules/tracker_library/tracker_library_screen.dart b/lib/modules/tracker_library/tracker_library_screen.dart index 536b7491..a363f36a 100644 --- a/lib/modules/tracker_library/tracker_library_screen.dart +++ b/lib/modules/tracker_library/tracker_library_screen.dart @@ -18,6 +18,7 @@ import 'package:mangayomi/modules/tracker_library/tracker_library_section.dart'; import 'package:mangayomi/modules/tracker_library/tracker_section_screen.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/utils/constant.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; enum TrackerProviders { @@ -92,7 +93,7 @@ class _TrackerLibraryScreenState extends ConsumerState { (trackerProvider.syncId == TrackerProviders.simkl.syncId || trackerProvider.syncId == TrackerProviders.trakt.syncId) ? trackerProvider.name - : "${trackerProvider.name} | ${itemType == ItemType.anime ? l10n.anime : l10n.manga}", + : "${trackerProvider.name} | ${itemType.localized(l10n)}", ), leading: !_isSearch ? null : Container(), actions: [ diff --git a/lib/modules/updates/updates_screen.dart b/lib/modules/updates/updates_screen.dart index 4f05927b..8eadbc37 100644 --- a/lib/modules/updates/updates_screen.dart +++ b/lib/modules/updates/updates_screen.dart @@ -1,23 +1,19 @@ -import 'package:bot_toast/bot_toast.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mangayomi/models/changed.dart'; -import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart'; import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; +import 'package:mangayomi/modules/widgets/base_library_tab_screen.dart'; import 'package:mangayomi/modules/widgets/custom_sliver_grouped_list_view.dart'; import 'package:isar_community/isar.dart'; -import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/update.dart'; import 'package:mangayomi/models/manga.dart'; -import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_providers.dart'; -import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/modules/updates/widgets/update_chapter_list_tile_widget.dart'; import 'package:mangayomi/modules/history/providers/isar_providers.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/services/library_updater.dart'; import 'package:mangayomi/utils/date.dart'; -import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart'; import 'package:mangayomi/modules/widgets/error_text.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; @@ -29,267 +25,104 @@ class UpdatesScreen extends ConsumerStatefulWidget { ConsumerState createState() => _UpdatesScreenState(); } -class _UpdatesScreenState extends ConsumerState - with TickerProviderStateMixin { - late TabController _tabBarController; - late final List _tabList; - late final List hideItems; +class _UpdatesScreenState extends BaseLibraryTabScreenState { bool _isLoading = false; - Future _updateLibrary() async { - setState(() { - _isLoading = true; - }); - bool isDark = ref.read(themeModeStateProvider); - botToast( - context.l10n.updating_library("0", "0", "0"), - fontSize: 13, - second: 30, - alignY: !context.isTablet ? 0.85 : 1, - themeDark: isDark, + + @override + String get title => l10nLocalizations(context)!.updates; + + @override + Widget buildTab(ItemType type) { + return Padding( + padding: const EdgeInsets.only(top: 10), + child: UpdateTab( + itemType: type, + query: textEditingController.text, + isLoading: _isLoading, + ), ); + } + + @override + Widget buildTabLabel(ItemType type, String label) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Tab(text: label), + const SizedBox(width: 8), + _updateNumbers(ref, type), + ], + ); + } + + @override + List buildExtraActions(BuildContext context) { + final l10n = l10nLocalizations(context)!; + + return [ + IconButton( + splashRadius: 20, + icon: Icon(Icons.refresh_outlined, color: Theme.of(context).hintColor), + onPressed: _updateLibrary, + ), + IconButton( + splashRadius: 20, + icon: Icon( + Icons.delete_sweep_outlined, + color: Theme.of(context).hintColor, + ), + onPressed: () { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.remove_everything), + content: Text(l10n.remove_all_update_msg), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () async { + Navigator.of(dialogContext).pop(); + await _clearUpdates(); + }, + child: Text(l10n.ok), + ), + ], + ), + ); + }, + ), + ]; + } + + Future _updateLibrary() async { + setState(() => _isLoading = true); + final itemType = getCurrentItemType(); final mangaList = isar.mangas .filter() .idIsNotNull() .favoriteEqualTo(true) .and() - .itemTypeEqualTo( - _tabBarController.index == 0 - ? ItemType.manga - : _tabBarController.index == 1 - ? ItemType.anime - : ItemType.novel, - ) + .itemTypeEqualTo(itemType) .and() .isLocalArchiveEqualTo(false) .findAllSync(); - int numbers = 0; - int failed = 0; - - for (var manga in mangaList) { - try { - await ref.read( - updateMangaDetailProvider( - mangaId: manga.id, - isInit: false, - showToast: false, - ).future, - ); - } catch (_) { - failed++; - } - numbers++; - if (mounted) { - botToast( - context.l10n.updating_library(numbers, failed, mangaList.length), - fontSize: 13, - second: 10, - alignY: !context.isTablet ? 0.85 : 1, - animationDuration: 0, - dismissDirections: [DismissDirection.none], - onlyOne: false, - themeDark: isDark, - ); - } - } - BotToast.cleanAll(); - setState(() { - _isLoading = false; - }); - } - - void tabListener() { - setState(() { - _textEditingController.clear(); - _isSearch = false; - }); - } - - @override - void dispose() { - _textEditingController.dispose(); - _tabBarController.dispose(); - super.dispose(); - } - - @override - void initState() { - super.initState(); - hideItems = ref.read(hideItemsStateProvider); - _tabList = [ - if (!hideItems.contains("/MangaLibrary")) "/MangaLibrary", - if (!hideItems.contains("/AnimeLibrary")) "/AnimeLibrary", - if (!hideItems.contains("/NovelLibrary")) "/NovelLibrary", - ]; - _tabBarController = TabController(length: _tabList.length, vsync: this); - _tabBarController.addListener(tabListener); - } - - final _textEditingController = TextEditingController(); - bool _isSearch = false; - @override - Widget build(BuildContext context) { - final l10n = l10nLocalizations(context)!; - return Scaffold( - appBar: AppBar( - elevation: 0, - backgroundColor: Colors.transparent, - title: _isSearch - ? null - : Text( - l10n.updates, - style: TextStyle(color: Theme.of(context).hintColor), - ), - actions: [ - _isSearch - ? SeachFormTextField( - onChanged: (value) { - setState(() {}); - }, - onSuffixPressed: () { - _textEditingController.clear(); - setState(() {}); - }, - onPressed: () { - setState(() { - _isSearch = false; - }); - _textEditingController.clear(); - }, - controller: _textEditingController, - ) - : IconButton( - splashRadius: 20, - onPressed: () { - setState(() { - _isSearch = true; - }); - }, - icon: Icon( - Icons.search_outlined, - color: Theme.of(context).hintColor, - ), - ), - IconButton( - splashRadius: 20, - onPressed: () { - _updateLibrary(); - }, - icon: Icon( - Icons.refresh_outlined, - color: Theme.of(context).hintColor, - ), - ), - IconButton( - splashRadius: 20, - onPressed: () { - showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Text(l10n.remove_everything), - content: Text(l10n.remove_all_update_msg), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - TextButton( - onPressed: () { - Navigator.pop(context); - }, - child: Text(l10n.cancel), - ), - const SizedBox(width: 15), - TextButton( - onPressed: () async { - if (mounted) Navigator.pop(context); - await _clearUpdates(hideItems); - }, - child: Text(l10n.ok), - ), - ], - ), - ], - ); - }, - ); - }, - icon: Icon( - Icons.delete_sweep_outlined, - color: Theme.of(context).hintColor, - ), - ), - ], - bottom: TabBar( - indicatorSize: TabBarIndicatorSize.tab, - controller: _tabBarController, - tabs: [ - if (!hideItems.contains("/MangaLibrary")) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Tab(text: l10n.manga), - const SizedBox(width: 8), - _updateNumbers(ref, ItemType.manga), - ], - ), - if (!hideItems.contains("/AnimeLibrary")) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Tab(text: l10n.anime), - const SizedBox(width: 8), - _updateNumbers(ref, ItemType.anime), - ], - ), - if (!hideItems.contains("/NovelLibrary")) - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Tab(text: l10n.novel), - const SizedBox(width: 8), - _updateNumbers(ref, ItemType.novel), - ], - ), - ], - ), - ), - body: Padding( - padding: const EdgeInsets.only(top: 10), - child: TabBarView( - controller: _tabBarController, - children: [ - if (!hideItems.contains("/MangaLibrary")) - UpdateTab( - itemType: ItemType.manga, - query: _textEditingController.text, - isLoading: _isLoading, - ), - if (!hideItems.contains("/AnimeLibrary")) - UpdateTab( - itemType: ItemType.anime, - query: _textEditingController.text, - isLoading: _isLoading, - ), - if (!hideItems.contains("/NovelLibrary")) - UpdateTab( - itemType: ItemType.novel, - query: _textEditingController.text, - isLoading: _isLoading, - ), - ], - ), - ), + await updateLibrary( + ref: ref, + context: context, + mangaList: mangaList, + itemType: itemType, ); + setState(() => _isLoading = false); } - Future _clearUpdates(List hideItems) async { + Future _clearUpdates() async { List updates = await isar.updates .filter() .idIsNotNull() - .chapter( - (q) => - q.manga((q) => q.itemTypeEqualTo(getCurrentItemType(hideItems))), - ) + .chapter((q) => q.manga((q) => q.itemTypeEqualTo(getCurrentItemType()))) .findAll(); final idsToDelete = []; isar.writeTxnSync(() { @@ -302,16 +135,6 @@ class _UpdatesScreenState extends ConsumerState }); await isar.writeTxn(() => isar.updates.deleteAll(idsToDelete)); } - - ItemType getCurrentItemType(List hideItems) { - return _tabBarController.index == 0 && !hideItems.contains("/MangaLibrary") - ? ItemType.manga - : _tabBarController.index == - 1 - (hideItems.contains("/MangaLibrary") ? 1 : 0) && - !hideItems.contains("/AnimeLibrary") - ? ItemType.anime - : ItemType.novel; - } } class UpdateTab extends ConsumerStatefulWidget { diff --git a/lib/modules/widgets/base_library_tab_screen.dart b/lib/modules/widgets/base_library_tab_screen.dart new file mode 100644 index 00000000..dbb75d3d --- /dev/null +++ b/lib/modules/widgets/base_library_tab_screen.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mangayomi/models/manga.dart'; +import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart'; +import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/utils/item_type_filters.dart'; +import 'package:mangayomi/utils/item_type_localization.dart'; + +abstract class BaseLibraryTabScreenState + extends ConsumerState + with TickerProviderStateMixin { + final textEditingController = TextEditingController(); + late TabController tabController; + late List visibleTabTypes; + late final List hideItems; + bool isSearch = false; + + /// Screen-specific title + String get title; + + /// Build the content of each tab + Widget buildTab(ItemType type); + + /// Optional extra actions (refresh, delete, etc.) + List buildExtraActions(BuildContext context) => []; + + /// Optional custom Tab widget (Updates needs this) + Widget buildTabLabel(ItemType type, String label) { + return Tab(text: label); + } + + @override + void initState() { + super.initState(); + hideItems = ref.read(hideItemsStateProvider); + + visibleTabTypes = hiddenItemTypes(hideItems); + + tabController = TabController(length: visibleTabTypes.length, vsync: this); + + tabController.addListener(() { + setState(() { + textEditingController.clear(); + isSearch = false; + }); + }); + } + + @override + void dispose() { + tabController.dispose(); + textEditingController.dispose(); + super.dispose(); + } + + ItemType getCurrentItemType() => visibleTabTypes[tabController.index]; + + @override + Widget build(BuildContext context) { + final l10n = l10nLocalizations(context)!; + return Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: Colors.transparent, + title: isSearch + ? null + : Text(title, style: TextStyle(color: Theme.of(context).hintColor)), + actions: [ + isSearch + ? SeachFormTextField( + controller: textEditingController, + onChanged: (_) => setState(() {}), + onSuffixPressed: () { + textEditingController.clear(); + setState(() {}); + }, + onPressed: () { + setState(() => isSearch = false); + textEditingController.clear(); + }, + ) + : IconButton( + splashRadius: 20, + onPressed: () => setState(() => isSearch = true), + icon: Icon(Icons.search, color: Theme.of(context).hintColor), + ), + ...buildExtraActions(context), + ], + bottom: TabBar( + controller: tabController, + indicatorSize: TabBarIndicatorSize.tab, + tabs: visibleTabTypes.map((type) { + return buildTabLabel(type, type.localized(l10n)); + }).toList(), + ), + ), + body: TabBarView( + controller: tabController, + children: visibleTabTypes.map(buildTab).toList(), + ), + ); + } +} diff --git a/lib/services/library_updater.dart b/lib/services/library_updater.dart new file mode 100644 index 00000000..47e069da --- /dev/null +++ b/lib/services/library_updater.dart @@ -0,0 +1,79 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter/material.dart'; +import 'package:bot_toast/bot_toast.dart'; +import 'package:mangayomi/eval/model/m_bridge.dart'; +import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_providers.dart'; +import 'package:mangayomi/modules/more/settings/appearance/providers/theme_mode_state_provider.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; +import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; +import 'package:mangayomi/utils/log/logger.dart'; +import 'package:mangayomi/models/manga.dart'; + +Future updateLibrary({ + required WidgetRef ref, + required BuildContext context, + required List mangaList, + required ItemType itemType, +}) async { + AppLogger.log("Starting ${itemType.name} library update..."); + if (mangaList.isEmpty) { + final cap = itemType.name[0].toUpperCase() + itemType.name.substring(1); + AppLogger.log("$cap library is empty. Nothing to update."); + return; + } + bool isDark = ref.read(themeModeStateProvider); + botToast( + context.l10n.updating_library("0", "0", "0"), + fontSize: 13, + second: 30, + alignY: !context.isTablet ? 0.85 : 1, + themeDark: isDark, + ); + int failed = 0; + List failedMangas = []; + for (var i = 0; i < mangaList.length; i++) { + final manga = mangaList[i]; + try { + await ref.read( + updateMangaDetailProvider( + mangaId: manga.id, + isInit: false, + showToast: false, + ).future, + ); + } catch (e) { + AppLogger.log( + "Failed to update ${itemType.name}:", + logLevel: LogLevel.error, + ); + AppLogger.log(e.toString(), logLevel: LogLevel.error); + failed++; + failedMangas.add(manga.name ?? "Unknown ${itemType.name}"); + } + if (context.mounted) { + botToast( + context.l10n.updating_library(i + 1, failed, mangaList.length), + fontSize: 13, + second: 10, + alignY: !context.isTablet ? 0.85 : 1, + animationDuration: 0, + dismissDirections: [DismissDirection.none], + onlyOne: false, + themeDark: isDark, + ); + } + } + await Future.delayed(const Duration(seconds: 1)); + BotToast.cleanAll(); + if (context.mounted && failedMangas.isNotEmpty) { + final failedListText = failedMangas.map((m) => "• $m").join('\n'); + final plural = failed == 1 ? itemType : "${itemType}s"; + botToast( + "Failed to update $failed $plural:\n$failedListText", + fontSize: 13, + second: 10, + alignY: !context.isTablet ? 0.85 : 1, + themeDark: isDark, + ); + } +} diff --git a/lib/utils/item_type_filters.dart b/lib/utils/item_type_filters.dart new file mode 100644 index 00000000..2db0d9c4 --- /dev/null +++ b/lib/utils/item_type_filters.dart @@ -0,0 +1,9 @@ +import 'package:mangayomi/models/manga.dart'; + +List hiddenItemTypes(List hideItems) { + return [ + if (!hideItems.contains("/MangaLibrary")) ItemType.manga, + if (!hideItems.contains("/AnimeLibrary")) ItemType.anime, + if (!hideItems.contains("/NovelLibrary")) ItemType.novel, + ]; +} diff --git a/lib/utils/item_type_localization.dart b/lib/utils/item_type_localization.dart new file mode 100644 index 00000000..7b122a46 --- /dev/null +++ b/lib/utils/item_type_localization.dart @@ -0,0 +1,37 @@ +import 'package:mangayomi/l10n/generated/app_localizations.dart'; +import 'package:mangayomi/models/manga.dart'; + +extension ItemTypeLocalization on ItemType { + String localized(AppLocalizations l10n) { + switch (this) { + case ItemType.manga: + return l10n.manga; + case ItemType.anime: + return l10n.anime; + case ItemType.novel: + return l10n.novel; + } + } + + String localizedSources(AppLocalizations l10n) { + switch (this) { + case ItemType.manga: + return l10n.manga_sources; + case ItemType.anime: + return l10n.anime_sources; + case ItemType.novel: + return l10n.novel_sources; + } + } + + String localizedExtensions(AppLocalizations l10n) { + switch (this) { + case ItemType.manga: + return l10n.manga_extensions; + case ItemType.anime: + return l10n.anime_extensions; + case ItemType.novel: + return l10n.novel_extensions; + } + } +}