Skip to content

Commit

Permalink
feat: improve search UI
Browse files Browse the repository at this point in the history
Migrate to using SearchAnchor.bar instead of custom search bar widget.
  • Loading branch information
Merrit committed Feb 9, 2024
1 parent abc34ec commit d7d89f4
Show file tree
Hide file tree
Showing 12 changed files with 183 additions and 138 deletions.
2 changes: 0 additions & 2 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ analyzer:
errors:
# Ignore lints for @JsonKey on Freezed classes
invalid_annotation_target: ignore
# Auto-removes unused imports with dart fix --apply
unused_import: error
# Exclude generated files from analysis
exclude:
- "**/*.g.dart"
Expand Down
1 change: 1 addition & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ void main(List<String> args) async {
repository: 'merrit/feeling_finder',
),
updateService: UpdateService(),
windowEvents: appWindow?.events,
);

// Initialize the settings service.
Expand Down
27 changes: 26 additions & 1 deletion lib/src/app.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';

import 'emoji/emoji_page.dart';
import 'localization/strings.g.dart';
import 'settings/cubit/settings_cubit.dart';
import 'settings/settings_page.dart';
import 'shortcuts/app_shortcuts.dart';
import 'theme/app_theme.dart';
import 'window/app_window.dart';

/// The base widget that configures the application.
class App extends StatefulWidget {
Expand All @@ -20,16 +23,26 @@ class App extends StatefulWidget {
State<App> createState() => _AppState();
}

class _AppState extends State<App> with TrayListener {
class _AppState extends State<App> with TrayListener, WindowListener {
late final AppWindow? appWindow;

@override
void initState() {
if (defaultTargetPlatform == TargetPlatform.linux ||
defaultTargetPlatform == TargetPlatform.windows ||
defaultTargetPlatform == TargetPlatform.macOS) {
appWindow = context.read<AppWindow>();
}

trayManager.addListener(this);
windowManager.addListener(this);
super.initState();
}

@override
void dispose() {
trayManager.removeListener(this);
windowManager.removeListener(this);
super.dispose();
}

Expand All @@ -45,6 +58,18 @@ class _AppState extends State<App> with TrayListener {
super.onTrayIconRightMouseDown();
}

@override
void onWindowBlur() {
appWindow?.addEvent(WindowEvent.unfocused);
super.onWindowBlur();
}

@override
void onWindowFocus() {
appWindow?.addEvent(WindowEvent.focused);
super.onWindowFocus();
}

@override
Widget build(BuildContext context) {
// The BlocBuilder widget will rebuild the
Expand Down
7 changes: 7 additions & 0 deletions lib/src/app/cubit/app_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import '../../logs/logging_manager.dart';
import '../../shortcuts/app_hotkey.dart';
import '../../storage/storage_service.dart';
import '../../updates/updates.dart';
import '../../window/app_window.dart';

part 'app_state.dart';
part 'app_cubit.freezed.dart';
Expand All @@ -25,13 +26,19 @@ class AppCubit extends Cubit<AppState> {
/// Service for fetching version info.
final UpdateService _updateService;

/// Stream of window events.
///
/// Will be null on non-desktop platforms.
final Stream<WindowEvent>? windowEvents;

/// Singleton instance.
static late AppCubit instance;

AppCubit(
this._storageService, {
required ReleaseNotesService releaseNotesService,
required UpdateService updateService,
required this.windowEvents,
}) : _updateService = updateService,
_releaseNotesService = releaseNotesService,
super(AppState.initial()) {
Expand Down
12 changes: 6 additions & 6 deletions lib/src/emoji/cubit/emoji_cubit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -87,16 +87,16 @@ class EmojiCubit extends Cubit<EmojiState> {
/// Search and filter for all emojis that match [searchString].
Future<void> search(String keyword) async {
if (keyword.isEmpty) {
// Keyword is empty when the user clears the search field, so we
// reset the list of emojis to the current category.
setCategory(state.category);
emit(state.copyWith(isSearching: false));
emit(state.copyWith(
searchResults: const [],
));

return;
}

final searchResults = _emojiService.search(keyword);
emit(state.copyWith(
emojis: _emojiService.search(keyword),
isSearching: true,
searchResults: searchResults,
));
}

Expand Down
6 changes: 3 additions & 3 deletions lib/src/emoji/cubit/emoji_state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ class EmojiState with _$EmojiState {
/// True if a list of recent emojis was loaded from storage.
required bool haveRecentEmojis,

/// True if a search is currently active.
required bool isSearching,
/// The list of emojis found by the search.
required List<Emoji> searchResults,
}) = _EmojiState;

factory EmojiState.initial(List<Emoji> recentEmojis, List<Emoji> smileys) {
Expand All @@ -26,7 +26,7 @@ class EmojiState with _$EmojiState {
category: (haveRecents) ? EmojiCategory.recent : EmojiCategory.smileys,
emojis: (haveRecents) ? recentEmojis : smileys,
haveRecentEmojis: haveRecents,
isSearching: false,
searchResults: const [],
);
}
}
127 changes: 39 additions & 88 deletions lib/src/emoji/emoji_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ class _EmojiPageState extends State<EmojiPage> {
final FocusNode searchBoxFocusNode = FocusNode(
debugLabel: 'searchBoxFocusNode',
);
final TextEditingController searchBoxTextController = TextEditingController();

final SearchController searchController = SearchController();

final FocusNode settingsButtonFocusNode = FocusNode(
debugLabel: 'settingsButtonFocusNode',
skipTraversal: false,
Expand All @@ -49,12 +51,6 @@ class _EmojiPageState extends State<EmojiPage> {

@override
Widget build(BuildContext context) {
final shortcuts = <ShortcutActivator, VoidCallback>{
const SingleActivator(LogicalKeyboardKey.enter): () => _handleEnterPressed(),
const SingleActivator(LogicalKeyboardKey.escape): () => _handleEscapePressed(),
const SingleActivator(LogicalKeyboardKey.tab): () => _handleTabPressed(),
};

return BlocBuilder<AppCubit, AppState>(
builder: (context, state) {
SchedulerBinding.instance.addPostFrameCallback((_) {
Expand All @@ -66,93 +62,47 @@ class _EmojiPageState extends State<EmojiPage> {
return FocusScope(
debugLabel: 'emojiPageFocusScope',
onKey: (node, event) => _redirectSearchKeys(event, searchBoxFocusNode),
child: CallbackShortcuts(
bindings: shortcuts,
child: BlocBuilder<EmojiCubit, EmojiState>(
buildWhen: (previous, current) => previous.category != current.category,
builder: (context, state) {
Widget? floatingActionButton;
if (state.category == EmojiCategory.custom) {
floatingActionButton = FloatingActionButton(
key: floatingActionButtonKey,
onPressed: () => _showAddCustomEmojiDialog(context),
child: const Icon(Icons.add),
);
}
child: BlocBuilder<EmojiCubit, EmojiState>(
buildWhen: (previous, current) => previous.category != current.category,
builder: (context, state) {
Widget? floatingActionButton;
if (state.category == EmojiCategory.custom) {
floatingActionButton = FloatingActionButton(
key: floatingActionButtonKey,
onPressed: () => _showAddCustomEmojiDialog(context),
child: const Icon(Icons.add),
);
}

return Scaffold(
appBar: AppBar(
centerTitle: true,
title: SearchBarWidget(
focusNode: searchBoxFocusNode,
textController: searchBoxTextController,
),
actions: [
_SettingsButton(focusNode: settingsButtonFocusNode),
],
return Scaffold(
appBar: AppBar(
centerTitle: true,
title: SearchBarWidget(
focusNode: searchBoxFocusNode,
searchController: searchController,
),
drawer: (platformIsMobile()) ? const Drawer(child: CategoryListView()) : null,
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Category buttons shown in a drawer on mobile.
if (!platformIsMobile()) const CategoryListView(),
EmojiGridView(floatingActionButtonKey, gridViewFocusNode),
],
),
floatingActionButton: floatingActionButton,
);
},
),
actions: [
_SettingsButton(focusNode: settingsButtonFocusNode),
],
),
drawer: (platformIsMobile()) ? const Drawer(child: CategoryListView()) : null,
body: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Category buttons shown in a drawer on mobile.
if (!platformIsMobile()) const CategoryListView(),
EmojiGridView(floatingActionButtonKey, gridViewFocusNode),
],
),
floatingActionButton: floatingActionButton,
);
},
),
);
},
);
}

/// Focus the grid view.
///
/// Will try to also focus the first emoji in the grid view.
void _focusGridView() {
gridViewFocusNode.requestFocus();
Future.delayed(const Duration(milliseconds: 10), () {
if (gridViewFocusNode.children.isEmpty) return;
gridViewFocusNode.children.first.requestFocus();
});
}

/// If the user presses the enter key while the search box is focused, focus
/// the grid view.
void _handleEnterPressed() {
if (searchBoxFocusNode.hasFocus) {
_focusGridView();
}
}

/// If the user presses the escape key, clear and unfocus the search box.
void _handleEscapePressed() {
searchBoxTextController.clear();
EmojiCubit.instance.search('');
_focusGridView();
}

/// Handles keyboard navigation with the tab key.
void _handleTabPressed() {
if (searchBoxFocusNode.hasFocus) {
// If the user presses the tab key while the search box is
// focused, focus the grid view.
_focusGridView();
} else if (gridViewFocusNode.hasFocus) {
// If the user presses the tab key while the grid view is
// focused, focus the AppBar's actions.
settingsButtonFocusNode.requestFocus();
} else if (settingsButtonFocusNode.hasFocus) {
// If the user presses the tab key while the AppBar's actions
// are focused, focus the search box.
searchBoxFocusNode.requestFocus();
}
}

/// Automatically focuses the search field when the user types.
KeyEventResult _redirectSearchKeys(
RawKeyEvent event,
Expand All @@ -168,6 +118,7 @@ class _EmojiPageState extends State<EmojiPage> {
LogicalKeyboardKey.arrowDown,
LogicalKeyboardKey.arrowLeft,
LogicalKeyboardKey.arrowRight,
LogicalKeyboardKey.control,
LogicalKeyboardKey.enter,
LogicalKeyboardKey.escape,
LogicalKeyboardKey.tab,
Expand All @@ -179,8 +130,8 @@ class _EmojiPageState extends State<EmojiPage> {
if (isNavigationKey) {
return KeyEventResult.ignored;
} else {
searchBoxTextController.text = event.character ?? '';
searchBoxFocusNode.requestFocus();
searchController.text = event.character ?? '';
searchController.openView();
return KeyEventResult.handled;
}
}
Expand Down
8 changes: 6 additions & 2 deletions lib/src/emoji/widgets/emoji_tile.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@ import '../styles.dart';
class EmojiTile extends StatefulWidget {
final Emoji emoji;
final int index;
final bool isSearchResult;

const EmojiTile(
this.emoji,
this.index, {
Key? key,
}) : super(key: key);
bool? isSearchResult,
}) : isSearchResult = isSearchResult ?? false,
super(key: key);

@override
State<EmojiTile> createState() => _EmojiTileState();
Expand Down Expand Up @@ -50,7 +53,8 @@ class _EmojiTileState extends State<EmojiTile> {

final bool categoryIsRecent = EmojiCubit.instance.state.category == EmojiCategory.recent;

final bool showVariantIndicator = hasVariants && (!categoryIsRecent || state.isSearching);
final bool showVariantIndicator =
hasVariants && (!categoryIsRecent || widget.isSearchResult);

final Decoration? hasVariantsIndicator;
if (showVariantIndicator) {
Expand Down
Loading

0 comments on commit d7d89f4

Please sign in to comment.