mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-04-21 07:41:58 +00:00
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:
commit
2365e28a1a
16 changed files with 595 additions and 776 deletions
|
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 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<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(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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: [
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
104
lib/modules/widgets/base_library_tab_screen.dart
Normal file
104
lib/modules/widgets/base_library_tab_screen.dart
Normal 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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
79
lib/services/library_updater.dart
Normal file
79
lib/services/library_updater.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
9
lib/utils/item_type_filters.dart
Normal file
9
lib/utils/item_type_filters.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
37
lib/utils/item_type_localization.dart
Normal file
37
lib/utils/item_type_localization.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue