From b8fffca2b321067bb9e163e3421fc512264a0faf Mon Sep 17 00:00:00 2001 From: Moustapha Kodjo Amadou <107993382+kodjodevf@users.noreply.github.com> Date: Sun, 9 Nov 2025 01:15:27 +0100 Subject: [PATCH] Refactor an fix --- .../widgets/btn_chapter_list_dialog.dart | 355 ++++++++++--- .../providers/auto_backup.dart | 1 + lib/modules/novel/novel_reader_view.dart | 1 + .../widgets/novel_reader_settings_sheet.dart | 498 +++++++++++++----- .../widgets/category_selection_dialog.dart | 395 +++++++++----- 5 files changed, 914 insertions(+), 336 deletions(-) diff --git a/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart b/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart index e6ce63aa..d1e4e4df 100644 --- a/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart +++ b/lib/modules/manga/reader/widgets/btn_chapter_list_dialog.dart @@ -2,10 +2,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; +import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/modules/manga/reader/providers/push_router.dart'; import 'package:mangayomi/modules/manga/reader/providers/reader_controller_provider.dart'; +import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/utils/date.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; +import 'package:mangayomi/utils/extensions/string_extensions.dart'; import 'package:super_sliver_list/super_sliver_list.dart'; Widget btnToShowChapterListDialog( @@ -21,11 +24,70 @@ Widget btnToShowChapterListDialog( await showDialog( context: context, builder: (context) { - return AlertDialog( - title: Text(title), - content: SizedBox( - width: context.width(0.8), - child: ChapterListWidget(chapter: chapter), + return Dialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + elevation: 8, + child: Container( + width: context.width(0.85), + constraints: BoxConstraints(maxHeight: context.height(0.8)), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + context.primaryColor.withValues(alpha: 0.05), + Colors.transparent, + ], + ), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: context.primaryColor.withValues(alpha: 0.2), + width: 1, + ), + ), + ), + child: Row( + children: [ + Icon( + Icons.menu_book_rounded, + color: context.primaryColor, + size: 24, + ), + const SizedBox(width: 12), + Expanded( + child: Text( + title, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: context.primaryColor, + ), + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon( + Icons.close_rounded, + color: context.primaryColor.withValues(alpha: 0.7), + ), + tooltip: 'Fermer', + ), + ], + ), + ), + Flexible(child: ChapterListWidget(chapter: chapter)), + ], + ), ), ); }, @@ -79,22 +141,25 @@ class _ChapterListWidgetState extends State { Widget build(BuildContext context) { return Scrollbar( interactive: true, - thickness: 12, + thickness: 8, radius: const Radius.circular(10), controller: controller, child: CustomScrollView( controller: controller, slivers: [ SliverPadding( - padding: const EdgeInsets.symmetric(vertical: 2), + padding: const EdgeInsets.all(12), sliver: SuperSliverList.builder( itemCount: chapterList.length, itemBuilder: (context, index) { final chapter = chapterList[index]; final currentChap = chapter == chapterList[currentChapIndex]; - return ChapterListTile( - chapter: chapter, - currentChap: currentChap, + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: ChapterListTile( + chapter: chapter, + currentChap: currentChap, + ), ); }, ), @@ -123,86 +188,224 @@ class _ChapterListTileState extends State { late bool isBookmarked = chapter.isBookmarked!; @override Widget build(BuildContext context) { - return Container( - color: widget.currentChap - ? context.primaryColor.withValues(alpha: 0.3) - : null, - child: ListTile( - textColor: chapter.isRead! - ? context.isLight - ? Colors.black.withValues(alpha: 0.4) - : Colors.white.withValues(alpha: 0.3) + return AnimatedContainer( + duration: const Duration(milliseconds: 200), + decoration: BoxDecoration( + color: widget.currentChap + ? context.primaryColor.withValues(alpha: 0.15) + : Colors.transparent, + borderRadius: BorderRadius.circular(12), + border: widget.currentChap + ? Border.all( + color: context.primaryColor.withValues(alpha: 0.4), + width: 1.5, + ) : null, - selectedColor: chapter.isRead! - ? Colors.white.withValues(alpha: 0.3) - : Colors.white, - onTap: () async { - if (!widget.currentChap) { - Navigator.pop(context); - pushReplacementMangaReaderView(context: context, chapter: chapter); - } - }, - title: Text( - chapter.name!, - style: const TextStyle(fontSize: 13), - overflow: TextOverflow.ellipsis, - ), - subtitle: Row( - children: [ - if (!(chapter.manga.value!.isLocalArchive ?? false)) - Consumer( - builder: (context, ref, child) => Text( - chapter.dateUpload == null || chapter.dateUpload!.isEmpty - ? "" - : dateFormat( - chapter.dateUpload!, - ref: ref, - context: context, - ), - style: const TextStyle(fontSize: 11), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: () async { + if (!widget.currentChap) { + Navigator.pop(context); + pushReplacementMangaReaderView( + context: context, + chapter: chapter, + ); + } + }, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + // Chapter indicator + Container( + width: 4, + height: 48, + decoration: BoxDecoration( + color: chapter.isRead! + ? Colors.grey.withValues(alpha: 0.3) + : context.primaryColor, + borderRadius: BorderRadius.circular(2), + ), ), - ), - if (!chapter.isRead!) - if (chapter.lastPageRead!.isNotEmpty && - chapter.lastPageRead != "1") - if (chapter.scanlator != null && chapter.scanlator!.isNotEmpty) - Row( + const SizedBox(width: 12), + // Chapter content + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - const Text(' • '), Text( - chapter.scanlator!, + chapter.name!, style: TextStyle( - fontSize: 11, + fontSize: 14, + fontWeight: widget.currentChap + ? FontWeight.bold + : FontWeight.w500, color: chapter.isRead! ? context.isLight ? Colors.black.withValues(alpha: 0.4) - : Colors.white.withValues(alpha: 0.3) + : Colors.white.withValues(alpha: 0.4) : null, ), + overflow: TextOverflow.ellipsis, + maxLines: 2, ), + const SizedBox(height: 4), + Row( + children: [ + if (!(chapter.manga.value!.isLocalArchive ?? false)) + Consumer( + builder: (context, ref, child) { + final dateText = + chapter.dateUpload == null || + chapter.dateUpload!.isEmpty + ? "" + : dateFormat( + chapter.dateUpload!, + ref: ref, + context: context, + ); + return dateText.isNotEmpty + ? Row( + children: [ + Icon( + Icons.access_time_rounded, + size: 12, + color: Colors.grey, + ), + const SizedBox(width: 4), + Text( + dateText, + style: const TextStyle( + fontSize: 11, + color: Colors.grey, + ), + ), + ], + ) + : const SizedBox.shrink(); + }, + ), + if (chapter.scanlator != null && + chapter.scanlator!.isNotEmpty) + Row( + children: [ + if (!(chapter.manga.value!.isLocalArchive ?? + false) && + (chapter.dateUpload != null && + chapter.dateUpload!.isNotEmpty)) + const Padding( + padding: EdgeInsets.symmetric( + horizontal: 6, + ), + child: Text( + '•', + style: TextStyle( + fontSize: 11, + color: Colors.grey, + ), + ), + ), + Icon( + Icons.group_rounded, + size: 12, + color: Colors.grey, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + chapter.scanlator!, + style: TextStyle( + fontSize: 11, + color: Colors.grey, + ), + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + if (!chapter.isRead! && + chapter.lastPageRead!.isNotEmpty && + chapter.lastPageRead != "1") + Padding( + padding: const EdgeInsets.only(top: 4), + child: Row( + children: [ + Icon( + Icons.bookmark_rounded, + size: 12, + color: context.primaryColor, + ), + const SizedBox(width: 4), + Text( + chapter.manga.value!.itemType == ItemType.anime + ? context.l10n.episode_progress( + Duration( + milliseconds: int.parse( + chapter.lastPageRead!, + ), + ).toString().substringBefore("."), + ) + : context.l10n.page( + chapter.manga.value!.itemType == + ItemType.manga + ? chapter.lastPageRead! + : "${((double.tryParse(chapter.lastPageRead!) ?? 0) * 100).toStringAsFixed(0)} %", + ), + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w500, + color: context.primaryColor, + ), + ), + ], + ), + ), ], ), - ], - ), - trailing: Consumer( - builder: (context, ref, child) => IconButton( - onPressed: () { - setState(() { - isBookmarked = !isBookmarked; - }); - isar.writeTxnSync( - () => { - isar.chapters.putSync( - chapter - ..isBookmarked = isBookmarked - ..updatedAt = DateTime.now().millisecondsSinceEpoch, + ), + const SizedBox(width: 12), + // Bookmark button + Consumer( + builder: (context, ref, child) => Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(20), + onTap: () { + setState(() { + isBookmarked = !isBookmarked; + }); + isar.writeTxnSync( + () => { + isar.chapters.putSync( + chapter + ..isBookmarked = isBookmarked + ..updatedAt = + DateTime.now().millisecondsSinceEpoch, + ), + }, + ); + }, + child: Padding( + padding: const EdgeInsets.all(8), + child: Icon( + isBookmarked + ? Icons.bookmark_rounded + : Icons.bookmark_outline_rounded, + color: isBookmarked + ? context.primaryColor + : Colors.grey, + size: 22, + ), + ), + ), ), - }, - ); - }, - icon: Icon( - isBookmarked ? Icons.bookmark : Icons.bookmark_outline, - color: context.primaryColor, + ), + ], ), ), ), diff --git a/lib/modules/more/data_and_storage/providers/auto_backup.dart b/lib/modules/more/data_and_storage/providers/auto_backup.dart index 57e6ca86..fa8370b7 100644 --- a/lib/modules/more/data_and_storage/providers/auto_backup.dart +++ b/lib/modules/more/data_and_storage/providers/auto_backup.dart @@ -79,6 +79,7 @@ class AutoBackupLocationState extends _$AutoBackupLocationState { @riverpod Future checkAndBackup(Ref ref) async { + ref.keepAlive(); final settings = isar.settings.getSync(227); final backupFrequency = _duration(settings!.backupFrequency); if (backupFrequency == null || settings.startDatebackup == null) return; diff --git a/lib/modules/novel/novel_reader_view.dart b/lib/modules/novel/novel_reader_view.dart index 073b3a12..8c7719f1 100644 --- a/lib/modules/novel/novel_reader_view.dart +++ b/lib/modules/novel/novel_reader_view.dart @@ -117,6 +117,7 @@ class _NovelWebViewState extends ConsumerState fontSize = initFontSize; }); }); + if (!isDesktop) SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersive); discordRpc?.showChapterDetails(ref, chapter); } diff --git a/lib/modules/novel/widgets/novel_reader_settings_sheet.dart b/lib/modules/novel/widgets/novel_reader_settings_sheet.dart index c53f3d97..3c664e09 100644 --- a/lib/modules/novel/widgets/novel_reader_settings_sheet.dart +++ b/lib/modules/novel/widgets/novel_reader_settings_sheet.dart @@ -172,22 +172,74 @@ class ReaderSettingsTab extends ConsumerWidget { children: [ Row( children: [ - const Icon(Icons.space_bar, size: 20), - Expanded( - child: Slider( - value: padding.toDouble(), - min: 0, - max: 50, - divisions: 50, - label: '$padding px', - onChanged: (value) { - ref - .read(novelReaderPaddingStateProvider.notifier) - .set(value.toInt()); - }, + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.space_bar_rounded, + size: 22, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 4, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + overlayShape: const RoundSliderOverlayShape( + overlayRadius: 16, + ), + activeTrackColor: Theme.of(context).primaryColor, + inactiveTrackColor: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.2), + thumbColor: Theme.of(context).primaryColor, + overlayColor: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.2), + ), + child: Slider( + value: padding.toDouble(), + min: 0, + max: 50, + divisions: 50, + label: '$padding px', + onChanged: (value) { + ref + .read(novelReaderPaddingStateProvider.notifier) + .set(value.toInt()); + }, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + '${padding}px', + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryColor, + ), ), ), - Text('${padding}px'), ], ), ], @@ -202,22 +254,76 @@ class ReaderSettingsTab extends ConsumerWidget { children: [ Row( children: [ - const Icon(Icons.height, size: 20), - Expanded( - child: Slider( - value: lineHeight, - min: 1.0, - max: 3.0, - divisions: 20, - label: lineHeight.toStringAsFixed(1), - onChanged: (value) { - ref - .read(novelReaderLineHeightStateProvider.notifier) - .set(value); - }, + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(10), + ), + child: Icon( + Icons.height_rounded, + size: 22, + color: Theme.of(context).primaryColor, + ), + ), + const SizedBox(width: 12), + Expanded( + child: SliderTheme( + data: SliderTheme.of(context).copyWith( + trackHeight: 4, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 8, + ), + overlayShape: const RoundSliderOverlayShape( + overlayRadius: 16, + ), + activeTrackColor: Theme.of(context).primaryColor, + inactiveTrackColor: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.2), + thumbColor: Theme.of(context).primaryColor, + overlayColor: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.2), + ), + child: Slider( + value: lineHeight, + min: 1.0, + max: 3.0, + divisions: 20, + label: lineHeight.toStringAsFixed(1), + onChanged: (value) { + ref + .read( + novelReaderLineHeightStateProvider.notifier, + ) + .set(value); + }, + ), + ), + ), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(8), + ), + child: Text( + lineHeight.toStringAsFixed(1), + style: TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + color: Theme.of(context).primaryColor, + ), ), ), - Text(lineHeight.toStringAsFixed(1)), ], ), ], @@ -328,19 +434,48 @@ class _SettingSection extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - color: Theme.of(context).textTheme.titleLarge?.color, + Padding( + padding: const EdgeInsets.only(left: 4, bottom: 10), + child: Row( + children: [ + Container( + width: 4, + height: 20, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + borderRadius: BorderRadius.circular(2), + ), + ), + const SizedBox(width: 10), + Text( + title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + color: Theme.of(context).textTheme.titleLarge?.color, + letterSpacing: 0.3, + ), + ), + ], ), ), - Card( - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 12), - child: child, + Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Theme.of(context).primaryColor.withValues(alpha: 0.04), + Colors.transparent, + ], + ), + border: Border.all( + color: Theme.of(context).primaryColor.withValues(alpha: 0.1), + width: 1, + ), ), + child: Padding(padding: const EdgeInsets.all(16), child: child), ), ], ); @@ -402,38 +537,58 @@ class _ThemeButton extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 70, - height: 60, - decoration: BoxDecoration( - color: _parseColor(backgroundColor), - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isSelected - ? Theme.of(context).primaryColor - : Colors.grey.withValues(alpha: 0.3), - width: isSelected ? 3 : 1, + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 75, + height: 70, + decoration: BoxDecoration( + color: _parseColor(backgroundColor), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isSelected + ? Theme.of(context).primaryColor + : Colors.grey.withValues(alpha: 0.3), + width: isSelected ? 3 : 1.5, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.3), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, ), - ), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Aa', - style: TextStyle( - color: _parseColor(textColor), - fontSize: 18, - fontWeight: FontWeight.bold, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Aa', + style: TextStyle( + color: _parseColor(textColor), + fontSize: 20, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(height: 4), - Text( - label, - style: TextStyle(color: _parseColor(textColor), fontSize: 10), - ), - ], + const SizedBox(height: 6), + Text( + label, + style: TextStyle( + color: _parseColor(textColor), + fontSize: 11, + fontWeight: FontWeight.w500, + ), + ), + ], + ), ), ), ); @@ -526,70 +681,127 @@ class _ColorPicker extends StatelessWidget { Color selectedColor, ) { final isSelected = optionColor.toARGB32() == selectedColor.toARGB32(); - return GestureDetector( - onTap: () { - onColorChanged(_colorToHex(optionColor)); - Navigator.of(context).pop(); - }, - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: optionColor, - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isSelected ? Theme.of(context).primaryColor : Colors.grey, - width: isSelected ? 3 : 1, + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + onColorChanged(_colorToHex(optionColor)); + Navigator.of(context).pop(); + }, + borderRadius: BorderRadius.circular(10), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 56, + height: 56, + decoration: BoxDecoration( + color: optionColor, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isSelected ? Theme.of(context).primaryColor : Colors.grey, + width: isSelected ? 3 : 1.5, + ), + boxShadow: isSelected + ? [ + BoxShadow( + color: Theme.of( + context, + ).primaryColor.withValues(alpha: 0.4), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ] + : null, ), + child: isSelected + ? Icon( + Icons.check_circle_rounded, + size: 26, + color: optionColor.computeLuminance() > 0.5 + ? Colors.black + : Colors.white, + ) + : null, ), - child: isSelected - ? Icon( - Icons.check, - color: optionColor.computeLuminance() > 0.5 - ? Colors.black - : Colors.white, - ) - : null, ), ); } @override Widget build(BuildContext context) { - return GestureDetector( - onTap: () => _showColorPickerDialog(context), - child: Container( - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - border: Border.all(color: Colors.grey.withValues(alpha: 0.3)), - borderRadius: BorderRadius.circular(8), - ), - child: Row( - children: [ - Container( - width: 30, - height: 30, - decoration: BoxDecoration( - color: _parseColor(color), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: Colors.grey), - ), + return Material( + color: Colors.transparent, + child: InkWell( + onTap: () => _showColorPickerDialog(context), + borderRadius: BorderRadius.circular(12), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + border: Border.all( + color: Colors.grey.withValues(alpha: 0.3), + width: 1.5, ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: const TextStyle(fontSize: 12)), - Text( - color, - style: TextStyle(fontSize: 10, color: Colors.grey[600]), + borderRadius: BorderRadius.circular(12), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Theme.of(context).primaryColor.withValues(alpha: 0.03), + Colors.transparent, + ], + ), + ), + child: Row( + children: [ + Container( + width: 36, + height: 36, + decoration: BoxDecoration( + color: _parseColor(color), + borderRadius: BorderRadius.circular(8), + border: Border.all( + color: Colors.grey.withValues(alpha: 0.5), + width: 2, ), - ], + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 4, + offset: const Offset(0, 2), + ), + ], + ), ), - ), - const Icon(Icons.arrow_drop_down), - ], + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + label, + style: const TextStyle( + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + color, + style: TextStyle( + fontSize: 11, + color: Colors.grey[600], + fontFamily: 'monospace', + ), + ), + ], + ), + ), + Icon( + Icons.palette_outlined, + color: Theme.of(context).primaryColor.withValues(alpha: 0.7), + size: 20, + ), + ], + ), ), ), ); @@ -609,29 +821,35 @@ class _AlignButton extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - width: 40, - height: 40, - decoration: BoxDecoration( - color: isSelected - ? Theme.of(context).primaryColor.withValues(alpha: 0.2) - : Colors.transparent, - borderRadius: BorderRadius.circular(8), - border: Border.all( + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(10), + child: AnimatedContainer( + duration: const Duration(milliseconds: 200), + width: 48, + height: 48, + decoration: BoxDecoration( + color: isSelected + ? Theme.of(context).primaryColor.withValues(alpha: 0.15) + : Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: isSelected + ? Theme.of(context).primaryColor + : Colors.grey.withValues(alpha: 0.3), + width: isSelected ? 2 : 1.5, + ), + ), + child: Icon( + icon, + size: 22, color: isSelected ? Theme.of(context).primaryColor - : Colors.grey.withValues(alpha: 0.3), - width: isSelected ? 2 : 1, + : Theme.of(context).iconTheme.color, ), ), - child: Icon( - icon, - color: isSelected - ? Theme.of(context).primaryColor - : Theme.of(context).iconTheme.color, - ), ), ); } diff --git a/lib/modules/widgets/category_selection_dialog.dart b/lib/modules/widgets/category_selection_dialog.dart index 32befdd0..d6168c3b 100644 --- a/lib/modules/widgets/category_selection_dialog.dart +++ b/lib/modules/widgets/category_selection_dialog.dart @@ -34,136 +34,291 @@ void showCategorySelectionDialog({ showDialog( context: context, builder: (context) => StatefulBuilder( - builder: (context, setState) => AlertDialog( - title: Text(l10n.set_categories), - content: SizedBox( - width: context.width(0.8), - child: StreamBuilder( - stream: isar.categorys - .filter() - .idIsNotNull() - .and() - .forItemTypeEqualTo(itemType) - .watch(fireImmediately: true), - builder: (context, snapshot) { - if (!snapshot.hasData || snapshot.data!.isEmpty) { - return Text(l10n.library_no_category_exist); - } - var entries = (snapshot.data! - ..sort((a, b) => (a.pos ?? 0).compareTo(b.pos ?? 0))); - if (isFavorite || isBulk) { - // When item is in library, hide hidden categories in list - entries = entries.where((e) => !(e.hide ?? false)).toList(); - } - if (entries.isEmpty) return Text(l10n.library_no_category_exist); - return SuperListView.builder( - shrinkWrap: true, - itemCount: entries.length, - itemBuilder: (context, index) { - final category = entries[index]; - final isSelected = categoryIds.contains(category.id); - if (!isBulk) { - return ListTileChapterFilter( - label: category.name!, - onTap: () { - setState(() { - isSelected - ? categoryIds.remove(category.id) - : categoryIds.add(category.id!); - }); - }, - type: isSelected ? 1 : 0, - ); - } - return ListTileMangaCategory( - category: category, - categoryIds: categoryIds, - mangasList: bulkMangas, - onTap: () { - setState(() { - if (isSelected) { - categoryIds.remove(category.id); - } else { - categoryIds.add(category.id!); - } - }); - }, - res: (res) { - if (res.isNotEmpty && !isSelected) { - categoryIds.add(category.id!); - } - }, - ); - }, - ); - }, + builder: (context, setState) => Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + elevation: 8, + child: Container( + width: context.width(0.85), + constraints: BoxConstraints(maxHeight: context.height(0.75)), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20), + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + context.primaryColor.withValues(alpha: 0.05), + Colors.transparent, + ], + ), ), - ), - actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - TextButton( - child: Text(l10n.edit), - onPressed: () { - context.push( - "/categories", - extra: ( - true, - itemType == ItemType.manga - ? 0 - : itemType == ItemType.anime - ? 1 - : 2, + // Header + Container( + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + border: Border( + bottom: BorderSide( + color: context.primaryColor.withValues(alpha: 0.2), + width: 1, ), - ); - Navigator.pop(context); - }, - ), - Row( - children: [ - TextButton( - child: Text(l10n.cancel), - onPressed: () => Navigator.pop(context), ), - const SizedBox(width: 15), - TextButton( - child: Text(l10n.ok), - onPressed: () { - isar.writeTxnSync(() { - if (isBulk) { - for (var manga in bulkMangas) { - manga.categories = categoryIds; - manga.updatedAt = - DateTime.now().millisecondsSinceEpoch; - isar.mangas.putSync(manga); + ), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: context.primaryColor.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + ), + child: Icon( + Icons.category_rounded, + color: context.primaryColor, + size: 24, + ), + ), + const SizedBox(width: 12), + Expanded( + child: Text( + l10n.set_categories, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: context.primaryColor, + ), + ), + ), + IconButton( + onPressed: () => Navigator.pop(context), + icon: Icon( + Icons.close_rounded, + color: context.primaryColor.withValues(alpha: 0.7), + ), + ), + ], + ), + ), + // Content + Flexible( + child: Padding( + padding: const EdgeInsets.all(16), + child: StreamBuilder( + stream: isar.categorys + .filter() + .idIsNotNull() + .and() + .forItemTypeEqualTo(itemType) + .watch(fireImmediately: true), + builder: (context, snapshot) { + if (!snapshot.hasData || snapshot.data!.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.category_outlined, + size: 64, + color: Colors.grey.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + l10n.library_no_category_exist, + style: TextStyle( + color: Colors.grey.withValues(alpha: 0.7), + fontSize: 15, + ), + ), + ], + ), + ); + } + var entries = (snapshot.data! + ..sort((a, b) => (a.pos ?? 0).compareTo(b.pos ?? 0))); + if (isFavorite || isBulk) { + // When item is in library, hide hidden categories in list + entries = entries + .where((e) => !(e.hide ?? false)) + .toList(); + } + if (entries.isEmpty) { + return Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.category_outlined, + size: 64, + color: Colors.grey.withValues(alpha: 0.5), + ), + const SizedBox(height: 16), + Text( + l10n.library_no_category_exist, + style: TextStyle( + color: Colors.grey.withValues(alpha: 0.7), + fontSize: 15, + ), + ), + ], + ), + ); + } + return SuperListView.builder( + shrinkWrap: true, + itemCount: entries.length, + itemBuilder: (context, index) { + final category = entries[index]; + final isSelected = categoryIds.contains(category.id); + if (!isBulk) { + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: ListTileChapterFilter( + label: category.name!, + onTap: () { + setState(() { + isSelected + ? categoryIds.remove(category.id) + : categoryIds.add(category.id!); + }); + }, + type: isSelected ? 1 : 0, + ), + ); } - } else { - if (!isFavorite) { - singleManga!.favorite = true; - singleManga.dateAdded = - DateTime.now().millisecondsSinceEpoch; - } - singleManga.categories = categoryIds; - singleManga.updatedAt = - DateTime.now().millisecondsSinceEpoch; - isar.mangas.putSync(singleManga); - } - if (isBulk) { - ref.read(mangasListStateProvider.notifier).clear(); - ref - .read(isLongPressedStateProvider.notifier) - .update(false); - } - }); - if (context.mounted) Navigator.pop(context); + return Padding( + padding: const EdgeInsets.only(bottom: 6), + child: ListTileMangaCategory( + category: category, + categoryIds: categoryIds, + mangasList: bulkMangas, + onTap: () { + setState(() { + if (isSelected) { + categoryIds.remove(category.id); + } else { + categoryIds.add(category.id!); + } + }); + }, + res: (res) { + if (res.isNotEmpty && !isSelected) { + categoryIds.add(category.id!); + } + }, + ), + ); + }, + ); }, ), - ], + ), + ), + // Actions + Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + border: Border( + top: BorderSide( + color: context.primaryColor.withValues(alpha: 0.1), + width: 1, + ), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + TextButton.icon( + icon: const Icon(Icons.edit_rounded, size: 18), + label: Text(l10n.edit), + style: TextButton.styleFrom( + foregroundColor: context.primaryColor, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onPressed: () { + context.push( + "/categories", + extra: ( + true, + itemType == ItemType.manga + ? 0 + : itemType == ItemType.anime + ? 1 + : 2, + ), + ); + Navigator.pop(context); + }, + ), + Row( + children: [ + TextButton( + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + ), + onPressed: () => Navigator.pop(context), + child: Text(l10n.cancel), + ), + const SizedBox(width: 12), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: context.primaryColor, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + ), + onPressed: () { + isar.writeTxnSync(() { + if (isBulk) { + for (var manga in bulkMangas) { + manga.categories = categoryIds; + manga.updatedAt = + DateTime.now().millisecondsSinceEpoch; + isar.mangas.putSync(manga); + } + } else { + if (!isFavorite) { + singleManga!.favorite = true; + singleManga.dateAdded = + DateTime.now().millisecondsSinceEpoch; + } + singleManga.categories = categoryIds; + singleManga.updatedAt = + DateTime.now().millisecondsSinceEpoch; + isar.mangas.putSync(singleManga); + } + if (isBulk) { + ref + .read(mangasListStateProvider.notifier) + .clear(); + ref + .read(isLongPressedStateProvider.notifier) + .update(false); + } + }); + if (context.mounted) Navigator.pop(context); + }, + child: Text(l10n.ok), + ), + ], + ), + ], + ), ), ], ), - ], + ), ), ), );