From 86fb19ecb22db2b9d1f947a975d8c3dc97d6618e Mon Sep 17 00:00:00 2001 From: NBA2K1 <78034913+NBA2K1@users.noreply.github.com> Date: Mon, 29 Dec 2025 03:28:04 +0100 Subject: [PATCH] Refactor tabbed screens into base class Reduce duplication --- lib/modules/history/history_screen.dart | 179 +++---------- lib/modules/updates/updates_screen.dart | 249 +++++------------- .../widgets/base_library_tab_screen.dart | 117 ++++++++ 3 files changed, 225 insertions(+), 320 deletions(-) create mode 100644 lib/modules/widgets/base_library_tab_screen.dart diff --git a/lib/modules/history/history_screen.dart b/lib/modules/history/history_screen.dart index 62954aa7..2d90e651 100644 --- a/lib/modules/history/history_screen.dart +++ b/lib/modules/history/history_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:mangayomi/l10n/generated/app_localizations.dart'; +import 'package:mangayomi/modules/widgets/base_library_tab_screen.dart'; import 'package:mangayomi/modules/widgets/custom_sliver_grouped_list_view.dart'; import 'package:isar_community/isar.dart'; @@ -12,7 +13,6 @@ import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/history.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/modules/history/providers/isar_providers.dart'; -import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/utils/cached_network.dart'; @@ -20,7 +20,6 @@ import 'package:mangayomi/utils/constant.dart'; import 'package:mangayomi/utils/date.dart'; import 'package:mangayomi/utils/extensions/chapter.dart'; import 'package:mangayomi/utils/headers.dart'; -import 'package:mangayomi/modules/library/widgets/search_text_form_field.dart'; import 'package:mangayomi/modules/widgets/error_text.dart'; import 'package:mangayomi/modules/widgets/progress_center.dart'; @@ -31,150 +30,50 @@ class HistoryScreen extends ConsumerStatefulWidget { ConsumerState createState() => _HistoryScreenState(); } -class _HistoryScreenState extends ConsumerState - with TickerProviderStateMixin { - final _textEditingController = TextEditingController(); - late TabController _tabBarController; - late List _visibleTabTypes; - late final List hideItems; +class _HistoryScreenState extends BaseLibraryTabScreenState { + @override + String get title => l10nLocalizations(context)!.history; - void tabListener() { - setState(() { - _textEditingController.clear(); - _isSearch = false; - }); + @override + Widget buildTab(ItemType type) { + return HistoryTab(itemType: type, query: textEditingController.text); } @override - void initState() { - super.initState(); - hideItems = ref.read(hideItemsStateProvider); - _visibleTabTypes = [ - if (!hideItems.contains("/MangaLibrary")) ItemType.manga, - if (!hideItems.contains("/AnimeLibrary")) ItemType.anime, - if (!hideItems.contains("/NovelLibrary")) ItemType.novel, - ]; - _tabBarController = TabController( - length: _visibleTabTypes.length, - vsync: this, - ); - _tabBarController.addListener(tabListener); - } - - @override - void dispose() { - _tabBarController.dispose(); - _textEditingController.dispose(); - super.dispose(); - } - - bool _isSearch = false; - @override - Widget build(BuildContext context) { + List buildExtraActions(BuildContext context) { final l10n = l10nLocalizations(context)!; - String localizedItemType(ItemType type) { - switch (type) { - case ItemType.manga: - return l10n.manga; - case ItemType.anime: - return l10n.anime; - case ItemType.novel: - return l10n.novel; - } - } - 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(); - }, - child: Text(l10n.ok), - ), - ], - ), - ], - ); - }, - ); - }, - icon: Icon( - Icons.delete_sweep_outlined, - color: Theme.of(context).hintColor, - ), - ), - ], - bottom: TabBar( - indicatorSize: TabBarIndicatorSize.tab, - controller: _tabBarController, - tabs: _visibleTabTypes.map((type) { - return Tab(text: localizedItemType(type)); - }).toList(), + 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: _visibleTabTypes.map((type) { - return HistoryTab(itemType: type, query: _textEditingController.text); - }).toList(), - ), - ); + ]; } Future _clearHistory() async { @@ -186,10 +85,6 @@ class _HistoryScreenState extends ConsumerState final List idsToDelete = histories.map((h) => h.id!).toList(); await isar.writeTxn(() => isar.historys.deleteAll(idsToDelete)); } - - ItemType getCurrentItemType() { - return _visibleTabTypes[_tabBarController.index]; - } } class HistoryTab extends ConsumerStatefulWidget { diff --git a/lib/modules/updates/updates_screen.dart b/lib/modules/updates/updates_screen.dart index 1c3315dc..8eadbc37 100644 --- a/lib/modules/updates/updates_screen.dart +++ b/lib/modules/updates/updates_screen.dart @@ -2,19 +2,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mangayomi/models/changed.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/main.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/update.dart'; import 'package:mangayomi/models/manga.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'; @@ -26,13 +25,78 @@ class UpdatesScreen extends ConsumerStatefulWidget { ConsumerState createState() => _UpdatesScreenState(); } -class _UpdatesScreenState extends ConsumerState - with TickerProviderStateMixin { - late TabController _tabBarController; - late final List _visibleTabTypes; - late final List hideItems; +class _UpdatesScreenState extends BaseLibraryTabScreenState { bool _isLoading = false; + @override + String get title => l10nLocalizations(context)!.updates; + + @override + Widget buildTab(ItemType type) { + return Padding( + padding: const EdgeInsets.only(top: 10), + child: UpdateTab( + itemType: type, + query: textEditingController.text, + isLoading: _isLoading, + ), + ); + } + + @override + Widget buildTabLabel(ItemType type, String label) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Tab(text: label), + const SizedBox(width: 8), + _updateNumbers(ref, type), + ], + ); + } + + @override + List buildExtraActions(BuildContext context) { + final l10n = l10nLocalizations(context)!; + + return [ + IconButton( + splashRadius: 20, + icon: Icon(Icons.refresh_outlined, color: Theme.of(context).hintColor), + onPressed: _updateLibrary, + ), + IconButton( + splashRadius: 20, + icon: Icon( + Icons.delete_sweep_outlined, + color: Theme.of(context).hintColor, + ), + onPressed: () { + showDialog( + context: context, + builder: (dialogContext) => AlertDialog( + title: Text(l10n.remove_everything), + content: Text(l10n.remove_all_update_msg), + actions: [ + TextButton( + onPressed: () => Navigator.of(dialogContext).pop(), + child: Text(l10n.cancel), + ), + TextButton( + onPressed: () async { + Navigator.of(dialogContext).pop(); + await _clearUpdates(); + }, + child: Text(l10n.ok), + ), + ], + ), + ); + }, + ), + ]; + } + Future _updateLibrary() async { setState(() => _isLoading = true); final itemType = getCurrentItemType(); @@ -54,173 +118,6 @@ class _UpdatesScreenState extends ConsumerState 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); - _visibleTabTypes = [ - if (!hideItems.contains("/MangaLibrary")) ItemType.manga, - if (!hideItems.contains("/AnimeLibrary")) ItemType.anime, - if (!hideItems.contains("/NovelLibrary")) ItemType.novel, - ]; - _tabBarController = TabController( - length: _visibleTabTypes.length, - vsync: this, - ); - _tabBarController.addListener(tabListener); - } - - final _textEditingController = TextEditingController(); - bool _isSearch = false; - @override - Widget build(BuildContext context) { - final l10n = l10nLocalizations(context)!; - String localizedItemType(ItemType type) { - switch (type) { - case ItemType.manga: - return l10n.manga; - case ItemType.anime: - return l10n.anime; - case ItemType.novel: - return l10n.novel; - } - } - - 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(); - }, - child: Text(l10n.ok), - ), - ], - ), - ], - ); - }, - ); - }, - icon: Icon( - Icons.delete_sweep_outlined, - color: Theme.of(context).hintColor, - ), - ), - ], - bottom: TabBar( - indicatorSize: TabBarIndicatorSize.tab, - controller: _tabBarController, - tabs: _visibleTabTypes.map((type) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Tab(text: localizedItemType(type)), - const SizedBox(width: 8), - _updateNumbers(ref, type), - ], - ); - }).toList(), - ), - ), - body: Padding( - padding: const EdgeInsets.only(top: 10), - child: TabBarView( - controller: _tabBarController, - children: _visibleTabTypes.map((type) { - return UpdateTab( - itemType: type, - query: _textEditingController.text, - isLoading: _isLoading, - ); - }).toList(), - ), - ), - ); - } - Future _clearUpdates() async { List updates = await isar.updates .filter() @@ -238,10 +135,6 @@ class _UpdatesScreenState extends ConsumerState }); await isar.writeTxn(() => isar.updates.deleteAll(idsToDelete)); } - - ItemType getCurrentItemType() { - return _visibleTabTypes[_tabBarController.index]; - } } class UpdateTab extends ConsumerStatefulWidget { diff --git a/lib/modules/widgets/base_library_tab_screen.dart b/lib/modules/widgets/base_library_tab_screen.dart new file mode 100644 index 00000000..f1a758d7 --- /dev/null +++ b/lib/modules/widgets/base_library_tab_screen.dart @@ -0,0 +1,117 @@ +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'; + +abstract class BaseLibraryTabScreenState + extends ConsumerState + with TickerProviderStateMixin { + final textEditingController = TextEditingController(); + late TabController tabController; + late List visibleTabTypes; + late final List hideItems; + bool isSearch = false; + + /// Screen-specific title + String get title; + + /// Build the content of each tab + Widget buildTab(ItemType type); + + /// Optional extra actions (refresh, delete, etc.) + List buildExtraActions(BuildContext context) => []; + + /// Optional custom Tab widget (Updates needs this) + Widget buildTabLabel(ItemType type, String label) { + return Tab(text: label); + } + + @override + void initState() { + super.initState(); + hideItems = ref.read(hideItemsStateProvider); + + visibleTabTypes = [ + if (!hideItems.contains("/MangaLibrary")) ItemType.manga, + if (!hideItems.contains("/AnimeLibrary")) ItemType.anime, + if (!hideItems.contains("/NovelLibrary")) ItemType.novel, + ]; + + 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)!; + String localizedItemType(ItemType type) { + switch (type) { + case ItemType.manga: + return l10n.manga; + case ItemType.anime: + return l10n.anime; + case ItemType.novel: + return l10n.novel; + } + } + + 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, localizedItemType(type)); + }).toList(), + ), + ), + body: TabBarView( + controller: tabController, + children: visibleTabTypes.map(buildTab).toList(), + ), + ); + } +}