diff --git a/app/lib/pages/pages_list.dart b/app/lib/pages/pages_list.dart index cb5c7cffd6..baeb7b8494 100644 --- a/app/lib/pages/pages_list.dart +++ b/app/lib/pages/pages_list.dart @@ -352,7 +352,7 @@ class _PageTile extends HookConsumerWidget { icon: FontAwesomeIcons.pen, onTap: () => showDialog( context: context, - builder: (_) => _RenamePageDialogue(old: pageId), + builder: (_) => RenamePageDialogue(old: pageId), ), ), ContextMenuTile.button( @@ -369,22 +369,7 @@ class _PageTile extends HookConsumerWidget { title: "Delete", icon: FontAwesomeIcons.trash, color: Colors.redAccent, - onTap: () => showConfirmationDialogue( - context: context, - title: "Delete ${pageId.formatted}?", - content: - "This will delete the page and all its content.\nTHIS CANNOT BE UNDONE.", - delayConfirm: 3.seconds, - confirmText: "Delete", - confirmIcon: FontAwesomeIcons.trash, - onConfirm: () async { - await ref.read(bookProvider.notifier).deletePage(pageId); - if (!isSelected) return; - unawaited( - ref.read(appRouter).replace(const EmptyPageEditorRoute()), - ); - }, - ), + onTap: () => showPageDeletionDialogue(context, ref.passing, pageId), ), ]; } @@ -657,9 +642,10 @@ class AddPageDialogue extends HookConsumerWidget { } } -class _RenamePageDialogue extends HookConsumerWidget { - const _RenamePageDialogue({ +class RenamePageDialogue extends HookConsumerWidget { + const RenamePageDialogue({ required this.old, + super.key, }); final String old; @@ -716,7 +702,7 @@ class _RenamePageDialogue extends HookConsumerWidget { onSubmitted: (value) async { final navigator = Navigator.of(context); await _renamePage(ref, value); - navigator.pop(); + navigator.pop(true); }, ), actions: [ @@ -726,7 +712,7 @@ class _RenamePageDialogue extends HookConsumerWidget { style: TextButton.styleFrom( foregroundColor: Theme.of(context).textTheme.bodySmall?.color, ), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => Navigator.of(context).pop(false), ), FilledButton.icon( onPressed: !isNameValid.value @@ -734,7 +720,7 @@ class _RenamePageDialogue extends HookConsumerWidget { : () async { final navigator = Navigator.of(context); await _renamePage(ref, controller.text); - navigator.pop(); + navigator.pop(true); }, label: const Text("Rename"), icon: const Icon(FontAwesomeIcons.pen), @@ -766,7 +752,7 @@ class ChangeChapterDialogue extends HookConsumerWidget { final navigator = Navigator.of(context); await ref.read(pageProvider(pageId))?.changeChapter(ref, newName); - navigator.pop(); + navigator.pop(true); } @override @@ -808,7 +794,7 @@ class ChangeChapterDialogue extends HookConsumerWidget { style: TextButton.styleFrom( foregroundColor: Theme.of(context).textTheme.bodySmall?.color, ), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => Navigator.of(context).pop(false), ), FilledButton.icon( onPressed: () async => _changeChapter( @@ -825,3 +811,25 @@ class ChangeChapterDialogue extends HookConsumerWidget { ); } } + +Future showPageDeletionDialogue( + BuildContext context, + PassingRef ref, + String pageId, +) { + return showConfirmationDialogue( + context: context, + title: "Delete ${pageId.formatted}?", + content: + "This will delete the page and all its content.\nTHIS CANNOT BE UNDONE.", + delayConfirm: 3.seconds, + confirmText: "Delete", + confirmIcon: FontAwesomeIcons.trash, + onConfirm: () async { + await ref.read(bookProvider.notifier).deletePage(pageId); + unawaited( + ref.read(appRouter).replace(const EmptyPageEditorRoute()), + ); + }, + ); +} diff --git a/app/lib/utils/popups.dart b/app/lib/utils/popups.dart index 48b9d9c380..dd92ff27b9 100644 --- a/app/lib/utils/popups.dart +++ b/app/lib/utils/popups.dart @@ -75,7 +75,7 @@ class ConfirmationDialogue extends HookWidget { icon: Icon(cancelIcon), label: Text(cancelText), onPressed: () { - Navigator.of(context).pop(); + Navigator.of(context).pop(false); onCancel?.call(); }, style: TextButton.styleFrom( @@ -90,7 +90,7 @@ class ConfirmationDialogue extends HookWidget { color: confirmColor, onPressed: canConfirm ? () { - Navigator.of(context).pop(); + Navigator.of(context).pop(true); onConfirm(); } : null, @@ -100,7 +100,7 @@ class ConfirmationDialogue extends HookWidget { } } -void showConfirmationDialogue({ +Future showConfirmationDialogue({ required BuildContext context, required Function onConfirm, String title = "Are you sure?", @@ -112,28 +112,30 @@ void showConfirmationDialogue({ String cancelText = "Cancel", IconData cancelIcon = FontAwesomeIcons.xmark, Function? onCancel, -}) { +}) async { // If the user has its shift key pressed, we skip the confirmation dialogue. // But only if the delay is 0. - final hasShiftDown = RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft); + final hasShiftDown = + RawKeyboard.instance.keysPressed.contains(LogicalKeyboardKey.shiftLeft); if (hasShiftDown && delayConfirm.inSeconds == 0) { onConfirm(); - return; + return true; } - showDialog( - context: context, - builder: (context) => ConfirmationDialogue( - onConfirm: onConfirm, - title: title, - content: content, - confirmText: confirmText, - confirmIcon: confirmIcon, - confirmColor: confirmColor, - delayConfirm: delayConfirm, - cancelText: cancelText, - cancelIcon: cancelIcon, - onCancel: onCancel, - ), - ); + return await showDialog( + context: context, + builder: (context) => ConfirmationDialogue( + onConfirm: onConfirm, + title: title, + content: content, + confirmText: confirmText, + confirmIcon: confirmIcon, + confirmColor: confirmColor, + delayConfirm: delayConfirm, + cancelText: cancelText, + cancelIcon: cancelIcon, + onCancel: onCancel, + ), + ) ?? + false; } diff --git a/app/lib/widgets/components/app/entry_search.dart b/app/lib/widgets/components/app/entry_search.dart index 9319fc7934..75e6043fcf 100644 --- a/app/lib/widgets/components/app/entry_search.dart +++ b/app/lib/widgets/components/app/entry_search.dart @@ -13,7 +13,6 @@ import "package:typewriter/utils/passing_reference.dart"; import "package:typewriter/utils/smart_single_activator.dart"; import "package:typewriter/widgets/components/app/page_search.dart"; import "package:typewriter/widgets/components/app/search_bar.dart"; -import "package:typewriter/widgets/components/general/toasts.dart"; import "package:typewriter/widgets/inspector/inspector.dart"; part "entry_search.g.dart"; @@ -202,8 +201,13 @@ class NewEntryFetcher extends SearchFetcher { final results = fuzzy.search(search.query); return results - .map((result) => - AddEntrySearchElement(result.item, onAdd: onAdd, onAdded: onAdded)) + .map( + (result) => AddEntrySearchElement( + result.item, + onAdd: onAdd, + onAdded: onAdded, + ), + ) .toList(); } @@ -320,7 +324,10 @@ class EntrySearchElement extends SearchElement { "Open Wiki", FontAwesomeIcons.book, SmartSingleActivator(LogicalKeyboardKey.keyO, control: true), - onTrigger: (_, __) => blueprint.openWiki(), + onTrigger: (_, __) { + blueprint.openWiki(); + return false; + }, ), ]; } @@ -371,7 +378,10 @@ class AddEntrySearchElement extends SearchElement { "Open Wiki", FontAwesomeIcons.book, SmartSingleActivator(LogicalKeyboardKey.keyO, control: true), - onTrigger: (_, __) => blueprint.openWiki(), + onTrigger: (_, __) { + blueprint.openWiki(); + return false; + }, ), ]; } diff --git a/app/lib/widgets/components/app/page_search.dart b/app/lib/widgets/components/app/page_search.dart index 59bca4d5c3..2b53cd08fb 100644 --- a/app/lib/widgets/components/app/page_search.dart +++ b/app/lib/widgets/components/app/page_search.dart @@ -1,6 +1,7 @@ import "package:collection/collection.dart"; import "package:flutter/material.dart" hide Page; import "package:flutter/services.dart"; +import "package:font_awesome_flutter/font_awesome_flutter.dart"; import "package:fuzzy/fuzzy.dart"; import "package:riverpod_annotation/riverpod_annotation.dart"; import "package:typewriter/app_router.dart"; @@ -8,6 +9,7 @@ import "package:typewriter/models/page.dart"; import "package:typewriter/pages/pages_list.dart"; import "package:typewriter/utils/extensions.dart"; import "package:typewriter/utils/passing_reference.dart"; +import "package:typewriter/utils/smart_single_activator.dart"; import "package:typewriter/widgets/components/app/search_bar.dart"; part "page_search.g.dart"; @@ -177,6 +179,39 @@ class PageSearchElement extends SearchElement { Icons.open_in_new, SingleActivator(LogicalKeyboardKey.enter), ), + SearchAction( + "Rename", + FontAwesomeIcons.pencil, + SmartSingleActivator(LogicalKeyboardKey.keyR, control: true), + onTrigger: (context, __) async => + await showDialog( + context: context, + builder: (_) => RenamePageDialogue(old: page.pageName), + ) ?? + false, + ), + SearchAction( + "Change Chapter", + FontAwesomeIcons.bookBookmark, + SmartSingleActivator(LogicalKeyboardKey.keyC, control: true), + onTrigger: (context, __) async => + await showDialog( + context: context, + builder: (_) => ChangeChapterDialogue( + pageId: page.pageName, + chapter: page.chapter, + ), + ) ?? + false, + ), + SearchAction( + "Delete", + FontAwesomeIcons.trash, + SmartSingleActivator(LogicalKeyboardKey.backspace, control: true), + color: Colors.red, + onTrigger: (context, ref) => + showPageDeletionDialogue(context, ref, page.pageName), + ), ]; } @@ -186,8 +221,12 @@ class PageSearchElement extends SearchElement { return await onSelect?.call(page) ?? true; } - await ref.read(appRouter).navigateToPage(ref, page.pageName); - return true; + final navigator = ref.read(appRouter); + + ref.read(searchProvider.notifier).endSearch(); + + await navigator.navigateToPage(ref, page.pageName); + return false; } } diff --git a/app/lib/widgets/components/app/search_bar.dart b/app/lib/widgets/components/app/search_bar.dart index 6fae0ab1bf..4dd60aaf19 100644 --- a/app/lib/widgets/components/app/search_bar.dart +++ b/app/lib/widgets/components/app/search_bar.dart @@ -233,12 +233,19 @@ Set _searchActionShortcuts(_SearchActionShortcutsRef ref) { } class SearchAction { - const SearchAction(this.name, this.icon, this.shortcut, {this.onTrigger}); + const SearchAction( + this.name, + this.icon, + this.shortcut, { + this.color, + this.onTrigger, + }); final String name; final IconData icon; final ShortcutActivator shortcut; - final Function(BuildContext, PassingRef ref)? onTrigger; + final Color? color; + final FutureOr Function(BuildContext, PassingRef ref)? onTrigger; } abstract class SearchElement { @@ -477,7 +484,9 @@ class _SearchFilters extends HookConsumerWidget { borderRadius: BorderRadius.circular(30), child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 12, vertical: 5), + horizontal: 12, + vertical: 5, + ), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -487,8 +496,10 @@ class _SearchFilters extends HookConsumerWidget { size: 16, ), const SizedBox(width: 8), - Text(fetcher.title, - style: const TextStyle(color: Colors.white)), + Text( + fetcher.title, + style: const TextStyle(color: Colors.white), + ), ], ), ), @@ -510,8 +521,10 @@ class _SearchFilters extends HookConsumerWidget { size: 16, ), const SizedBox(width: 8), - Text(filter.title, - style: const TextStyle(color: Colors.white)), + Text( + filter.title, + style: const TextStyle(color: Colors.white), + ), if (filter.canRemove) ...[ const SizedBox(width: 12), IconButton( @@ -613,11 +626,11 @@ class _SearchResults extends HookConsumerWidget { List actions, int index, BuildContext context, - WidgetRef ref, + PassingRef ref, ) async { if (index >= actions.length) return; if (index < 0) return; - final canEnd = await actions[index].activate(context, ref.passing); + final canEnd = await actions[index].activate(context, ref); if (canEnd) ref.read(searchProvider.notifier).endSearch(); } @@ -638,7 +651,8 @@ class _SearchResults extends HookConsumerWidget { for (var i = 0; i < elements.length; i++) _ResultTile( key: globalKeys[i], - onPressed: () => _activateItem(elements, i, context, ref), + onPressed: () => + _activateItem(elements, i, context, ref.passing), focusNode: focusNodes[i], title: elements[i].title, color: elements[i].color(context), @@ -708,15 +722,16 @@ class _ResultTile extends HookConsumerWidget { final String description; final List actions; - void _invokeAction( + Future _invokeAction( BuildContext context, PassingRef ref, ActivateActionIntent intent, - ) { + ) async { final action = actions .firstWhereOrNull((action) => action.shortcut == intent.shortcut); if (action == null) return; - action.onTrigger?.call(context, ref); + final canEnd = await action.onTrigger?.call(context, ref) ?? false; + if (canEnd) ref.read(searchProvider.notifier).endSearch(); } @override @@ -731,6 +746,7 @@ class _ResultTile extends HookConsumerWidget { ContextMenuTile.button( title: action.name, icon: action.icon, + color: action.color, onTap: () => action.onTrigger!(context, ref.passing), ), ]; @@ -890,7 +906,8 @@ class _SearchActions extends HookConsumerWidget { ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( "Actions", @@ -899,12 +916,15 @@ class _SearchActions extends HookConsumerWidget { .bodySmall ?.copyWith(fontWeight: FontWeight.bold), ), - const Spacer(), - for (final action in actions) ...[ - _SearchBarAction(action: action), - const SizedBox(width: 8), - ], - const Spacer(), + const SizedBox(height: 8), + Wrap( + spacing: 8, + runSpacing: 8, + children: [ + for (final action in actions) _SearchBarAction(action: action), + ], + ), + const SizedBox(height: 8), ], ), ), @@ -922,21 +942,30 @@ class _SearchBarAction extends HookConsumerWidget { final SearchAction action; + Future _invokeAction( + BuildContext context, + PassingRef ref, + ) async { + final canEnd = await action.onTrigger?.call(context, ref) ?? false; + if (canEnd) ref.read(searchProvider.notifier).endSearch(); + } + @override Widget build(BuildContext context, WidgetRef ref) { + final borderColor = action.color ?? Theme.of(context).dividerColor; return Material( elevation: 0, color: Theme.of(context).cardColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(4), side: BorderSide( - color: Theme.of(context).dividerColor, + color: borderColor, width: 1, ), ), child: InkWell( onTap: action.onTrigger != null - ? () => action.onTrigger!(context, ref.passing) + ? () => _invokeAction(context, ref.passing) : null, borderRadius: BorderRadius.circular(4), child: Padding( @@ -944,6 +973,12 @@ class _SearchBarAction extends HookConsumerWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ + FaIcon( + action.icon, + color: borderColor, + size: 12, + ), + const SizedBox(width: 8), Text(action.name), const SizedBox(width: 6), ShortcutLabel(activator: action.shortcut), diff --git a/app/lib/widgets/components/general/shortcut_label.dart b/app/lib/widgets/components/general/shortcut_label.dart index 13aa637114..7b16746d98 100644 --- a/app/lib/widgets/components/general/shortcut_label.dart +++ b/app/lib/widgets/components/general/shortcut_label.dart @@ -27,7 +27,12 @@ class ShortcutLabel extends HookConsumerWidget { Widget? _overrideIcon() { if (activator is SingleActivator) { final act = activator as SingleActivator; - if (act.trigger == LogicalKeyboardKey.enter) return const Icon(CupertinoIcons.return_icon); + if (act.trigger == LogicalKeyboardKey.enter) { + return const Icon(CupertinoIcons.return_icon); + } + if (act.trigger == LogicalKeyboardKey.backspace) { + return const Icon(CupertinoIcons.delete_left); + } } return null; }