Merge pull request #636 from NBA2K1/main

Fix "Show extensions" Button AND Fix category rearrange bug AND hide ItemType from calendar
This commit is contained in:
Moustapha Kodjo Amadou 2026-01-05 11:55:33 +01:00 committed by GitHub
commit 2365e28a1a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 595 additions and 776 deletions

View file

@ -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<BrowseScreen> createState() => _BrowseScreenState();
}
enum BrowseTabKind { sources, extensions }
class BrowseTab {
final ItemType type;
final BrowseTabKind kind;
const BrowseTab(this.type, this.kind);
}
class _BrowseScreenState extends ConsumerState<BrowseScreen>
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<BrowseTab> _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<BrowseScreen>
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<BrowseScreen>
)
: 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<BrowseScreen>
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<BrowseScreen>
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<BrowseScreen>
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(),
),
),
);

View file

@ -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<BrowseTab> 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<SourcesScreen> {
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),
),

View file

@ -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,

View file

@ -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<CalendarScreen> {
DateTime? _selectedDay;
DateTime? _rangeStart;
DateTime? _rangeEnd;
late ItemType? itemType = widget.itemType ?? ItemType.manga;
late ItemType? itemType;
late List<ItemType> _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<CalendarScreen> {
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<CalendarScreen> {
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<CalendarScreen> {
],
),
),
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<CalendarScreen> {
);
}
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<Manga> 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<String, List<Manga>> _dayCache = {};
List<Manga> _getEntriesForDay(DateTime day, List<Manga> 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<CalendarScreen> {
? 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<Manga> _getEntriesForRange(

View file

@ -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<HistoryScreen> createState() => _HistoryScreenState();
}
class _HistoryScreenState extends ConsumerState<HistoryScreen>
with TickerProviderStateMixin {
final _textEditingController = TextEditingController();
late TabController _tabBarController;
class _HistoryScreenState extends BaseLibraryTabScreenState<HistoryScreen> {
@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<Widget> 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<void> _clearHistory(List<String> hideItems) async {
Future<void> _clearHistory() async {
List<History> 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<Id> idsToDelete = histories.map((h) => h.id!).toList();
await isar.writeTxn(() => isar.historys.deleteAll(idsToDelete));
}
ItemType getCurrentItemType(List<String> 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 {

View file

@ -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<LibraryScreen>
super.dispose();
}
Future<void> _updateLibrary(List<Manga> 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<LibraryScreen>
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<LibraryScreen>
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<LibraryScreen>
: 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<LibraryScreen>
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) {

View file

@ -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<MangaHomeScreen> {
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(

View file

@ -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<CategoriesScreen>
with TickerProviderStateMixin {
late TabController _tabBarController;
late final List<String> _tabList;
late final List<ItemType> _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<CategoriesScreen>
@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<CategoriesScreen>
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<CategoriesScreen>
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<CategoriesTab> {
List<Category> _entries = [];
void _updateCategoriesOrder(List<Category> 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<void> _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 inmemory 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<CategoriesTab> {
),
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<CategoriesTab> {
),
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<CategoriesTab> {
),
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(

View file

@ -175,17 +175,13 @@ Future<void> 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,

View file

@ -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<StatisticsScreen>
with SingleTickerProviderStateMixin {
late final List<String> hideItems;
late TabController _tabController;
late final List<String> _tabList;
late final List<ItemType> _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<StatisticsScreen>
@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<StatisticsScreen>
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<StatisticsScreen>
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<StatisticsScreen>
downloadedItems: downloadedItems,
averageChapters: averageChapters.toDouble(),
readPercentage: readPercentage.toDouble(),
title: title,
title: itemType.localized(l10n),
context: context,
unreadLabel: unreadLabel,
),

View file

@ -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<TrackerLibraryScreen> {
(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: [

View file

@ -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<UpdatesScreen> createState() => _UpdatesScreenState();
}
class _UpdatesScreenState extends ConsumerState<UpdatesScreen>
with TickerProviderStateMixin {
late TabController _tabBarController;
late final List<String> _tabList;
late final List<String> hideItems;
class _UpdatesScreenState extends BaseLibraryTabScreenState<UpdatesScreen> {
bool _isLoading = false;
Future<void> _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<Widget> 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<void> _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<void> _clearUpdates(List<String> hideItems) async {
Future<void> _clearUpdates() async {
List<Update> 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 = <Id>[];
isar.writeTxnSync(() {
@ -302,16 +135,6 @@ class _UpdatesScreenState extends ConsumerState<UpdatesScreen>
});
await isar.writeTxn(() => isar.updates.deleteAll(idsToDelete));
}
ItemType getCurrentItemType(List<String> 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 {

View file

@ -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<T extends ConsumerStatefulWidget>
extends ConsumerState<T>
with TickerProviderStateMixin {
final textEditingController = TextEditingController();
late TabController tabController;
late List<ItemType> visibleTabTypes;
late final List<String> 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<Widget> 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(),
),
);
}
}

View file

@ -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<void> updateLibrary({
required WidgetRef ref,
required BuildContext context,
required List<Manga> 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<String> 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,
);
}
}

View file

@ -0,0 +1,9 @@
import 'package:mangayomi/models/manga.dart';
List<ItemType> hiddenItemTypes(List<String> hideItems) {
return [
if (!hideItems.contains("/MangaLibrary")) ItemType.manga,
if (!hideItems.contains("/AnimeLibrary")) ItemType.anime,
if (!hideItems.contains("/NovelLibrary")) ItemType.novel,
];
}

View file

@ -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;
}
}
}