Extract reusable Select Bar and Button widgets

Previously, the same select bar and button styles were defined in both
`library_screen.dart` and `manga_detail_view.dart`, resulting in repeated code.

This commit extracts the select bar and its buttons into reusable widgets
to reduce duplication and improve readability and maintainability.
This commit is contained in:
NBA2K1 2025-07-28 16:29:30 +02:00
parent ae1d158264
commit c2bae6d17b
3 changed files with 371 additions and 478 deletions

View file

@ -22,6 +22,7 @@ import 'package:mangayomi/modules/manga/detail/providers/update_manga_detail_pro
import 'package:mangayomi/modules/more/categories/providers/isar_providers.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';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/modules/widgets/manga_image_card_widget.dart';
@ -554,161 +555,85 @@ class _LibraryScreenState extends ConsumerState<LibraryScreen>
return const ProgressCenter();
},
),
bottomNavigationBar: Consumer(
builder: (context, ref, child) {
final isLongPressed = ref.watch(isLongPressedMangaStateProvider);
final color = Theme.of(context).textTheme.bodyLarge!.color!;
bottomNavigationBar: Builder(
builder: (context) {
final mangaIds = ref.watch(mangasListStateProvider);
return AnimatedContainer(
curve: Curves.easeIn,
decoration: BoxDecoration(
color: context.primaryColor.withValues(alpha: 0.2),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
final color = Theme.of(context).textTheme.bodyLarge!.color!;
return BottomSelectBar(
isVisible: ref.watch(isLongPressedMangaStateProvider),
actions: [
BottomSelectButton(
icon: Icon(Icons.label_outline_rounded, color: color),
onPressed: () {
final mangaIdsList = ref.watch(mangasListStateProvider);
final List<Manga> bulkMangas = mangaIdsList
.map((id) => isar.mangas.getSync(id)!)
.toList();
showCategorySelectionDialog(
context: context,
ref: ref,
itemType: widget.itemType,
bulkMangas: bulkMangas,
);
},
),
),
duration: const Duration(milliseconds: 100),
height: isLongPressed ? 70 : 0,
width: context.width(1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
shadowColor: Colors.transparent,
elevation: 0,
backgroundColor: Colors.transparent,
),
onPressed: () {
final mangaIdsList = ref.watch(
mangasListStateProvider,
);
final List<Manga> bulkMangas = mangaIdsList
.map((id) => isar.mangas.getSync(id)!)
.toList();
showCategorySelectionDialog(
context: context,
ref: ref,
itemType: widget.itemType,
bulkMangas: bulkMangas,
);
},
child: Icon(
Icons.label_outline_rounded,
color: color,
),
BottomSelectButton(
icon: Icon(Icons.done_all_sharp, color: color),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: true,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
),
),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: true,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
);
},
child: Icon(Icons.done_all_sharp, color: color),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
),
),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: false,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
);
},
child: Icon(Icons.remove_done_sharp, color: color),
);
},
),
BottomSelectButton(
icon: Icon(Icons.remove_done_sharp, color: color),
onPressed: () {
ref
.read(
mangasSetIsReadStateProvider(
mangaIds: mangaIds,
markAsRead: false,
).notifier,
)
.set();
ref.invalidate(
getAllMangaWithoutCategoriesStreamProvider(
itemType: widget.itemType,
),
),
),
// Expanded(
// child: SizedBox(
// height: 70,
// child: ElevatedButton(
// style: ElevatedButton.styleFrom(
// elevation: 0,
// backgroundColor: Colors.transparent,
// shadowColor: Colors.transparent,
// ),
// onPressed: () {},
// child: Icon(
// Icons.download_outlined,
// color: color,
// )),
// ),
// ),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
_deleteManga();
},
child: Icon(
Icons.delete_outline_outlined,
color: color,
),
);
ref.invalidate(
getAllMangaStreamProvider(
categoryId: null,
itemType: widget.itemType,
),
),
),
],
),
);
},
),
// BottomBarAction(
// icon: Icon(Icons.download_outlined, color: color),
// onPressed: () {}
// ),
BottomSelectButton(
icon: Icon(Icons.delete_outline_outlined, color: color),
onPressed: () => _deleteManga(),
),
],
);
},
),

View file

@ -24,6 +24,7 @@ import 'package:mangayomi/modules/manga/detail/widgets/tracker_widget.dart';
import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart';
import 'package:mangayomi/modules/more/settings/appearance/providers/pure_black_dark_mode_state_provider.dart';
import 'package:mangayomi/modules/more/settings/track/widgets/track_listile.dart';
import 'package:mangayomi/modules/widgets/bottom_select_bar.dart';
import 'package:mangayomi/modules/widgets/category_selection_dialog.dart';
import 'package:mangayomi/modules/widgets/custom_draggable_tabbar.dart';
import 'package:mangayomi/modules/widgets/custom_extended_image_provider.dart';
@ -811,8 +812,8 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
],
),
),
bottomNavigationBar: Consumer(
builder: (context, ref, child) {
bottomNavigationBar: Builder(
builder: (context) {
final chap = ref.watch(chaptersListStateProvider);
bool getLength1 = chap.length == 1;
bool checkFirstBookmarked =
@ -821,340 +822,241 @@ class _MangaDetailViewState extends ConsumerState<MangaDetailView>
chap.isNotEmpty && chap.first.isRead! && getLength1;
final l10n = l10nLocalizations(context)!;
final color = Theme.of(context).textTheme.bodyLarge!.color!;
return AnimatedContainer(
curve: Curves.easeIn,
decoration: BoxDecoration(
color: context.primaryColor.withValues(alpha: 0.2),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
return BottomSelectBar(
isVisible: isLongPressed,
actions: [
BottomSelectButton(
icon: Icon(
checkFirstBookmarked
? Icons.bookmark_remove_outlined
: Icons.bookmark_add_outlined,
color: color,
),
onPressed: () {
final chapters = ref.watch(chaptersListStateProvider);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
for (var chapter in chapters) {
chapter.isBookmarked = !chapter.isBookmarked!;
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref.read(chaptersListStateProvider.notifier).clear();
},
),
),
duration: const Duration(milliseconds: 100),
height: isLongPressed ? 70 : 0,
width: context.width(1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
final chapters = ref.watch(
chaptersListStateProvider,
);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
for (var chapter in chapters) {
chapter.isBookmarked = !chapter.isBookmarked!;
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Icon(
checkFirstBookmarked
? Icons.bookmark_remove_outlined
: Icons.bookmark_add_outlined,
color: color,
),
),
),
BottomSelectButton(
icon: Icon(
checkReadBookmarked
? Icons.remove_done_sharp
: Icons.done_all_sharp,
color: color,
),
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
final chapters = ref.watch(
chaptersListStateProvider,
);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
for (var chapter in chapters) {
chapter.isRead = !chapter.isRead!;
if (!chapter.isRead!) {
chapter.lastPageRead = "1";
}
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
if (chapter.isRead!) {
chapter.updateTrackChapterRead(ref);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
isar.mangas.putSync(widget.manga!);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Icon(
checkReadBookmarked
? Icons.remove_done_sharp
: Icons.done_all_sharp,
color: color,
),
),
),
),
if (getLength1)
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
int index = chapters.indexOf(chap.first);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
chapters[index + 1].updateTrackChapterRead(ref);
for (
var i = index + 1;
i < chapters.length;
i++
) {
final chapter = chapters[i];
if (!chapter.isRead!) {
chapter.isRead = true;
chapter.lastPageRead = "1";
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
isar.mangas.putSync(widget.manga!);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Stack(
children: [
Icon(Icons.done_outlined, color: color),
Positioned(
bottom: 0,
right: 0,
child: Icon(
Icons.arrow_downward_outlined,
size: 11,
color: color,
),
),
],
),
),
),
),
if (!isLocalArchive)
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
for (var chapter in ref.watch(
chaptersListStateProvider,
)) {
final entries = isar.downloads
.filter()
.idEqualTo(chapter.id)
.findAllSync();
if (entries.isEmpty ||
!entries.first.isDownload!) {
ref.read(
addDownloadToQueueProvider(
chapter: chapter,
),
);
}
}
ref.watch(processDownloadsProvider());
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref
.read(chaptersListStateProvider.notifier)
.clear();
},
child: Icon(Icons.download_outlined, color: color),
),
),
),
if (isLocalArchive)
Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: () {
final selectedChapters = ref.watch(
chaptersListStateProvider,
);
final totalChapters =
widget.manga!.chapters.length;
final isLastChapters =
selectedChapters.length == totalChapters;
final isAnime = widget.itemType == ItemType.anime;
final entryType = isAnime
? l10n.episode
: l10n.chapter;
final pluralEntryType = isAnime
? l10n.episodes
: l10n.chapters;
final mediaType = isAnime
? l10n.anime
: l10n.manga;
final warningMessage = l10n
.last_entry_delete_warning(
totalChapters,
entryType,
pluralEntryType,
mediaType,
);
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.delete_chapters),
content: isLastChapters
? Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
),
const SizedBox(width: 12),
Expanded(
child: Text(
warningMessage,
style: TextStyle(
color: Colors.red,
),
),
),
],
)
: null,
actions: [
Row(
mainAxisAlignment:
MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () async {
final navigator = Navigator.of(
context,
);
await isar.writeTxn(() async {
final idsToDelete =
selectedChapters
.map((c) => c.id!)
.toList();
await isar.chapters.deleteAll(
idsToDelete,
);
});
if (!mounted) return;
ref
.read(
isLongPressedStateProvider
.notifier,
)
.update(false);
ref
.read(
chaptersListStateProvider
.notifier,
)
.clear();
navigator.pop();
if (isLastChapters) {
navigator.pop();
Future.delayed(
const Duration(
milliseconds: 350,
),
() {
isar.writeTxn(
() => isar.mangas.delete(
widget.manga!.id!,
),
);
},
);
}
},
child: Text(l10n.delete),
),
],
),
],
);
},
);
},
onPressed: () {
final chapters = ref.watch(chaptersListStateProvider);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
for (var chapter in chapters) {
chapter.isRead = !chapter.isRead!;
if (!chapter.isRead!) {
chapter.lastPageRead = "1";
}
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
if (chapter.isRead!) {
chapter.updateTrackChapterRead(ref);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
isar.mangas.putSync(widget.manga!);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref.read(chaptersListStateProvider.notifier).clear();
},
),
if (getLength1)
BottomSelectButton(
icon: Stack(
children: [
Icon(Icons.done_outlined, color: color),
Positioned(
bottom: 0,
right: 0,
child: Icon(
Icons.delete_outline_outlined,
Icons.arrow_downward_outlined,
size: 11,
color: color,
),
),
),
],
),
],
),
onPressed: () {
int index = chapters.indexOf(chap.first);
final List<Chapter> updatedChapters = [];
final now = DateTime.now().millisecondsSinceEpoch;
chapters[index + 1].updateTrackChapterRead(ref);
for (var i = index + 1; i < chapters.length; i++) {
final chapter = chapters[i];
if (!chapter.isRead!) {
chapter.isRead = true;
chapter.lastPageRead = "1";
chapter.updatedAt = now;
chapter.manga.value = widget.manga;
updatedChapters.add(chapter);
}
}
isar.writeTxnSync(() {
isar.chapters.putAllSync(updatedChapters);
isar.mangas.putSync(widget.manga!);
});
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref.read(chaptersListStateProvider.notifier).clear();
},
),
if (!isLocalArchive)
BottomSelectButton(
icon: Icon(Icons.download_outlined, color: color),
onPressed: () {
for (var chapter in ref.watch(
chaptersListStateProvider,
)) {
final entries = isar.downloads
.filter()
.idEqualTo(chapter.id)
.findAllSync();
if (entries.isEmpty || !entries.first.isDownload!) {
ref.read(
addDownloadToQueueProvider(chapter: chapter),
);
}
}
ref.watch(processDownloadsProvider());
ref
.read(isLongPressedStateProvider.notifier)
.update(false);
ref.read(chaptersListStateProvider.notifier).clear();
},
),
if (isLocalArchive)
BottomSelectButton(
icon: Icon(Icons.delete_outline_outlined, color: color),
onPressed: () {
final selectedChapters = ref.watch(
chaptersListStateProvider,
);
final totalChapters = widget.manga!.chapters.length;
final isLastChapters =
selectedChapters.length == totalChapters;
final isAnime = widget.itemType == ItemType.anime;
final entryType = isAnime ? l10n.episode : l10n.chapter;
final pluralEntryType = isAnime
? l10n.episodes
: l10n.chapters;
final mediaType = isAnime ? l10n.anime : l10n.manga;
final warningMessage = l10n.last_entry_delete_warning(
totalChapters,
entryType,
pluralEntryType,
mediaType,
);
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text(l10n.delete_chapters),
content: isLastChapters
? Row(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
const Icon(
Icons.warning_amber_rounded,
color: Colors.orange,
),
const SizedBox(width: 12),
Expanded(
child: Text(
warningMessage,
style: TextStyle(color: Colors.red),
),
),
],
)
: null,
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () {
Navigator.pop(context);
},
child: Text(l10n.cancel),
),
const SizedBox(width: 15),
TextButton(
onPressed: () async {
final navigator = Navigator.of(context);
await isar.writeTxn(() async {
final idsToDelete = selectedChapters
.map((c) => c.id!)
.toList();
await isar.chapters.deleteAll(
idsToDelete,
);
});
if (!mounted) return;
ref
.read(
isLongPressedStateProvider
.notifier,
)
.update(false);
ref
.read(
chaptersListStateProvider
.notifier,
)
.clear();
navigator.pop();
if (isLastChapters) {
navigator.pop();
Future.delayed(
const Duration(milliseconds: 350),
() {
isar.writeTxn(
() => isar.mangas.delete(
widget.manga!.id!,
),
);
},
);
}
},
child: Text(l10n.delete),
),
],
),
],
);
},
);
},
),
],
);
},
),

View file

@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
import 'package:mangayomi/utils/extensions/build_context_extensions.dart';
/// Bar, that appears at the bottom of the screen when long-pressing (selecting)
/// a Manga/Anime/Novel or Chapter/Episode
class BottomSelectBar extends StatelessWidget {
final bool isVisible;
final List<BottomSelectButton> actions;
const BottomSelectBar({
super.key,
required this.isVisible,
required this.actions,
});
@override
Widget build(BuildContext context) {
return AnimatedContainer(
curve: Curves.easeIn,
decoration: BoxDecoration(
color: context.primaryColor.withValues(alpha: 0.2),
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
),
),
duration: const Duration(milliseconds: 100),
height: isVisible ? 70 : 0,
width: context.width(1),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: actions,
),
);
}
}
/// Button for the BottomSelectBar
class BottomSelectButton extends StatelessWidget {
final Widget icon;
final VoidCallback onPressed;
const BottomSelectButton({
super.key,
required this.icon,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Expanded(
child: SizedBox(
height: 70,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
elevation: 0,
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
),
onPressed: onPressed,
child: icon,
),
),
);
}
}