mirror of
https://github.com/kodjodevf/mangayomi.git
synced 2026-01-11 22:40:36 +00:00
Refactor tabbed screens into base class
Reduce duplication
This commit is contained in:
parent
bdcd28488e
commit
86fb19ecb2
3 changed files with 225 additions and 320 deletions
|
|
@ -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<HistoryScreen> createState() => _HistoryScreenState();
|
||||
}
|
||||
|
||||
class _HistoryScreenState extends ConsumerState<HistoryScreen>
|
||||
with TickerProviderStateMixin {
|
||||
final _textEditingController = TextEditingController();
|
||||
late TabController _tabBarController;
|
||||
late List<ItemType> _visibleTabTypes;
|
||||
late final List<String> hideItems;
|
||||
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();
|
||||
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<Widget> 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<void> _clearHistory() async {
|
||||
|
|
@ -186,10 +85,6 @@ class _HistoryScreenState extends ConsumerState<HistoryScreen>
|
|||
final List<Id> idsToDelete = histories.map((h) => h.id!).toList();
|
||||
await isar.writeTxn(() => isar.historys.deleteAll(idsToDelete));
|
||||
}
|
||||
|
||||
ItemType getCurrentItemType() {
|
||||
return _visibleTabTypes[_tabBarController.index];
|
||||
}
|
||||
}
|
||||
|
||||
class HistoryTab extends ConsumerStatefulWidget {
|
||||
|
|
|
|||
|
|
@ -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<UpdatesScreen> createState() => _UpdatesScreenState();
|
||||
}
|
||||
|
||||
class _UpdatesScreenState extends ConsumerState<UpdatesScreen>
|
||||
with TickerProviderStateMixin {
|
||||
late TabController _tabBarController;
|
||||
late final List<ItemType> _visibleTabTypes;
|
||||
late final List<String> hideItems;
|
||||
class _UpdatesScreenState extends BaseLibraryTabScreenState<UpdatesScreen> {
|
||||
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<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();
|
||||
|
|
@ -54,173 +118,6 @@ class _UpdatesScreenState extends ConsumerState<UpdatesScreen>
|
|||
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<void> _clearUpdates() async {
|
||||
List<Update> updates = await isar.updates
|
||||
.filter()
|
||||
|
|
@ -238,10 +135,6 @@ class _UpdatesScreenState extends ConsumerState<UpdatesScreen>
|
|||
});
|
||||
await isar.writeTxn(() => isar.updates.deleteAll(idsToDelete));
|
||||
}
|
||||
|
||||
ItemType getCurrentItemType() {
|
||||
return _visibleTabTypes[_tabBarController.index];
|
||||
}
|
||||
}
|
||||
|
||||
class UpdateTab extends ConsumerStatefulWidget {
|
||||
|
|
|
|||
117
lib/modules/widgets/base_library_tab_screen.dart
Normal file
117
lib/modules/widgets/base_library_tab_screen.dart
Normal file
|
|
@ -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<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 = [
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue