diff --git a/frontend/appflowy_flutter/integration_test/mobile/search/search_test.dart b/frontend/appflowy_flutter/integration_test/mobile/search/search_test.dart index 3031d5eaa0..9510af6786 100644 --- a/frontend/appflowy_flutter/integration_test/mobile/search/search_test.dart +++ b/frontend/appflowy_flutter/integration_test/mobile/search/search_test.dart @@ -36,7 +36,7 @@ void main() { expect(find.byType(MobileSearchRecentList), findsNothing); expect(find.byType(MobileSearchResultList), findsOneWidget); expect( - find.text(LocaleKeys.search_noResultForSearching.tr(args: [query])), + find.text(LocaleKeys.search_noResultForSearching.tr()), findsOneWidget, ); diff --git a/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart index d4e3e2cc44..2a7e893e58 100644 --- a/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart +++ b/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart @@ -47,9 +47,10 @@ class NotificationReminderBloc status: NotificationReminderStatus.error, ), ); + return; } - final layout = view!.layout; + final layout = view.layout; if (layout.isDocumentView) { final node = await _getContent(reminder); diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart index c2920ef480..aec7054c4f 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_edit_link_widget.dart @@ -163,6 +163,7 @@ class _MobileBottomSheetEditLinkWidgetState decoration: LinkStyle.buildLinkTextFieldInputDecoration( LocaleKeys.document_toolbar_linkNameHint.tr(), contentPadding: EdgeInsets.all(14), + radius: 12, context, ), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_cell.dart b/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_cell.dart index 318c02d638..48a45d7bd6 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_cell.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_cell.dart @@ -1,12 +1,9 @@ -import 'package:appflowy/generated/flowy_svgs.g.dart'; -import 'package:appflowy/generated/locale_keys.g.dart'; import 'package:appflowy/workspace/application/command_palette/command_palette_bloc.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; -import 'package:easy_localization/easy_localization.dart'; import 'package:flowy_infra_ui/widget/spacing.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -28,7 +25,7 @@ class MobileSearchResultCell extends StatelessWidget { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - buildIcon(), + buildIcon(theme), HSpace(12), Flexible( child: Column( @@ -57,54 +54,34 @@ class MobileSearchResultCell extends StatelessWidget { ); } - Widget buildIcon() { + Widget buildIcon(AppFlowyThemeData theme) { final icon = item.icon; if (icon.ty == ResultIconTypePB.Emoji) { - return icon.getIcon(size: 16.0, lineHeight: 20 / 16) ?? SizedBox.shrink(); - } else { return icon.getIcon(size: 20) ?? SizedBox.shrink(); + } else { + return icon.getIcon(size: 20, iconColor: theme.iconColorScheme.primary) ?? + SizedBox.shrink(); } } Widget buildPath(CommandPaletteState state, AppFlowyThemeData theme) { - bool isInTrash = false; - for (final view in state.trash) { - if (view.id == item.id) { - isInTrash = true; - break; - } - } - if (isInTrash) { - return Row( - children: [ - const FlowySvg(FlowySvgs.trash_s, size: Size.square(20)), - const HSpace(4.0), - Text( - '${LocaleKeys.trash_text.tr()} / ${item.displayName}', + return BlocProvider( + create: (context) => ViewAncestorBloc(item.id), + child: BlocBuilder( + builder: (context, state) { + final ancestors = state.ancestor.ancestors; + List displayPath = ancestors.map((e) => e.name).toList(); + if (ancestors.length > 2) { + displayPath = [ancestors.first.name, '...', ancestors.last.name]; + } + return Text( + displayPath.join(' / '), style: theme.textStyle.body .standard(color: theme.textColorScheme.secondary), - ), - ], - ); - } else { - return BlocProvider( - create: (context) => ViewAncestorBloc(item.id), - child: BlocBuilder( - builder: (context, state) { - final ancestors = state.ancestor.ancestors; - List displayPath = ancestors.map((e) => e.name).toList(); - if (ancestors.length > 2) { - displayPath = [ancestors.first.name, '...', ancestors.last.name]; - } - return Text( - displayPath.join(' / '), - style: theme.textStyle.body - .standard(color: theme.textColorScheme.secondary), - ); - }, - ), - ); - } + ); + }, + ), + ); } Widget buildSummary(AppFlowyThemeData theme) { diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_page.dart index 6db6017e8b..6da0194df5 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_page.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_page.dart @@ -61,16 +61,11 @@ class MobileSearchScreen extends StatelessWidget { ), ), ); - return Scaffold( - body: SafeArea( - bottom: false, - child: Provider.value( - value: userProfile, - child: MobileSearchPage( - userProfile: userProfile, - workspaceLatestPB: latest, - ), - ), + return Provider.value( + value: userProfile, + child: MobileSearchPage( + userProfile: userProfile, + workspaceLatestPB: latest, ), ); }, @@ -78,7 +73,7 @@ class MobileSearchScreen extends StatelessWidget { } } -class MobileSearchPage extends StatelessWidget { +class MobileSearchPage extends StatefulWidget { const MobileSearchPage({ super.key, required this.userProfile, @@ -88,31 +83,62 @@ class MobileSearchPage extends StatelessWidget { final UserProfilePB userProfile; final WorkspaceLatestPB workspaceLatestPB; + @override + State createState() => _MobileSearchPageState(); +} + +class _MobileSearchPageState extends State { bool get enableShowAISearch => - userProfile.workspaceType == WorkspaceTypePB.ServerW; + widget.userProfile.workspaceType == WorkspaceTypePB.ServerW; + + final focusNode = FocusNode(); + + @override + void dispose() { + focusNode.dispose(); + super.dispose(); + } @override Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MobileSearchTextfield( - hintText: enableShowAISearch - ? LocaleKeys.search_searchOrAskAI.tr() - : LocaleKeys.search_label.tr(), - query: state.query ?? '', - onChanged: (value) => context - .read() - .add(CommandPaletteEvent.searchChanged(search: value)), + return SafeArea( + child: Scaffold( + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MobileSearchTextfield( + focusNode: focusNode, + hintText: enableShowAISearch + ? LocaleKeys.search_searchOrAskAI.tr() + : LocaleKeys.search_label.tr(), + query: state.query ?? '', + onChanged: (value) => + context.read().add( + CommandPaletteEvent.searchChanged(search: value), + ), + ), + if (enableShowAISearch) + MobileSearchAskAiEntrance(query: state.query), + Flexible( + child: NotificationListener( + child: MobileSearchResult(), + onNotification: (t) { + if (t is ScrollUpdateNotification) { + if (focusNode.hasFocus) { + focusNode.unfocus(); + } + } + return true; + }, + ), + ), + ], ), - if (enableShowAISearch) - MobileSearchAskAiEntrance(query: state.query), - Flexible(child: SafeArea(child: MobileSearchResult())), - ], + ), ), ); }, diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_result.dart b/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_result.dart index 36f0fb2d14..9adb3f7ea7 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_result.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_result.dart @@ -6,6 +6,7 @@ import 'package:appflowy/workspace/application/command_palette/command_palette_b import 'package:appflowy/workspace/application/command_palette/search_result_list_bloc.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/view/view_service.dart'; +import 'package:appflowy_backend/log.dart'; import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart'; import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_ui/appflowy_ui.dart'; @@ -36,13 +37,18 @@ class MobileSearchRecentList extends StatelessWidget { @override Widget build(BuildContext context) { final theme = AppFlowyTheme.of(context); + final commandPaletteState = context.read().state; + + final trashIdSet = commandPaletteState.trash.map((e) => e.id).toSet(); return BlocProvider( create: (context) => RecentViewsBloc()..add(const RecentViewsEvent.initial()), child: BlocBuilder( builder: (context, state) { - final List recentViews = - state.views.map((e) => e.item).toSet().toList(); + final List recentViews = state.views + .map((e) => e.item) + .where((e) => !trashIdSet.contains(e.id)) + .toList(); return SingleChildScrollView( child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -115,7 +121,7 @@ class _MobileSearchResultListState extends State { children: [ const VSpace(16), Text( - LocaleKeys.search_bestMatch.tr(), + LocaleKeys.commandPalette_bestMatches.tr(), style: theme.textStyle.body .enhanced(color: theme.textColorScheme.secondary), ), @@ -130,6 +136,10 @@ class _MobileSearchResultListState extends State { .fold((s) => s, (s) => null); if (view != null && context.mounted) { await _goToView(context, view); + } else { + Log.error( + 'tapping search result, view not found: ${item.id}', + ); } }, child: MobileSearchResultCell( @@ -161,12 +171,13 @@ class _MobileSearchResultListState extends State { ), const VSpace(12), Text( - LocaleKeys.search_noResultForSearching.tr(args: [query]), + LocaleKeys.search_noResultForSearching.tr(), style: theme.textStyle.body.enhanced(color: textColor), maxLines: 1, overflow: TextOverflow.ellipsis, ), Text( + textAlign: TextAlign.center, LocaleKeys.search_noResultForSearchingHint.tr(), style: theme.textStyle.caption.standard(color: textColor), ), diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_textfield.dart b/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_textfield.dart index 19a30e708e..06e7f6be1b 100644 --- a/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_textfield.dart +++ b/frontend/appflowy_flutter/lib/mobile/presentation/search/mobile_search_textfield.dart @@ -11,11 +11,13 @@ class MobileSearchTextfield extends StatefulWidget { this.onChanged, required this.hintText, required this.query, + required this.focusNode, }); final String hintText; final String query; final ValueChanged? onChanged; + final FocusNode focusNode; @override State createState() => _MobileSearchTextfieldState(); @@ -23,18 +25,15 @@ class MobileSearchTextfield extends StatefulWidget { class _MobileSearchTextfieldState extends State { late final TextEditingController controller; - late final FocusNode focusNode; final ValueNotifier hasFocusValueNotifier = ValueNotifier(true); + FocusNode get focusNode => widget.focusNode; + @override void initState() { super.initState(); controller = TextEditingController(text: widget.query); - focusNode = FocusNode(); - focusNode.addListener(() { - if (!mounted) return; - hasFocusValueNotifier.value = focusNode.hasFocus; - }); + focusNode.addListener(onFocusChanged); controller.addListener(() { if (!mounted) return; widget.onChanged?.call(controller.text); @@ -46,7 +45,7 @@ class _MobileSearchTextfieldState extends State { @override void dispose() { controller.dispose(); - focusNode.dispose(); + focusNode.removeListener(onFocusChanged); hasFocusValueNotifier.dispose(); bottomNavigationBarItemType.removeListener(onBackOrLeave); super.dispose(); @@ -157,11 +156,17 @@ class _MobileSearchTextfieldState extends State { ); } + void onFocusChanged() { + if (!mounted) return; + hasFocusValueNotifier.value = focusNode.hasFocus; + } + void onBackOrLeave() { final label = bottomNavigationBarItemType.value; if (label == BottomNavigationBarItemType.search.label) { focusNode.requestFocus(); } else { + focusNode.unfocus(); controller.clear(); } } diff --git a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart index dfd56af03b..3b6712e1d1 100644 --- a/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart +++ b/frontend/appflowy_flutter/lib/plugins/document/presentation/editor_plugins/desktop_toolbar/link/link_styles.dart @@ -11,10 +11,11 @@ class LinkStyle { BuildContext context, { bool showErrorBorder = false, EdgeInsets? contentPadding, + double? radius, }) { final theme = AppFlowyTheme.of(context); final border = OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(8.0)), + borderRadius: BorderRadius.all(Radius.circular(radius ?? 8.0)), borderSide: BorderSide(color: borderColor(context)), ); final enableBorder = border.copyWith( diff --git a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart index a248c4787a..0b386cded3 100644 --- a/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart +++ b/frontend/appflowy_flutter/lib/workspace/application/command_palette/search_result_ext.dart @@ -5,7 +5,11 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:flutter/material.dart'; extension GetIcon on ResultIconPB { - Widget? getIcon({double size = 18.0, double lineHeight = 1.0}) { + Widget? getIcon({ + double size = 18.0, + double lineHeight = 1.0, + Color? iconColor, + }) { final iconValue = value, iconType = ty; if (iconType == ResultIconTypePB.Emoji) { return iconValue.isNotEmpty @@ -17,7 +21,11 @@ extension GetIcon on ResultIconPB { : null; } else if (ty == ResultIconTypePB.Icon) { if (_resultIconValueTypes.contains(iconValue)) { - return FlowySvg(getViewSvg(), size: Size.square(size)); + return FlowySvg( + getViewSvg(), + size: Size.square(size), + color: iconColor, + ); } return RawEmojiIconWidget( emoji: EmojiIconData(iconType.toFlowyIconType(), iconValue), diff --git a/frontend/resources/translations/en.json b/frontend/resources/translations/en.json index bd9e6e26d3..9f15e1b05e 100644 --- a/frontend/resources/translations/en.json +++ b/frontend/resources/translations/en.json @@ -2347,8 +2347,8 @@ "searchOrAskAI": "Search or ask AI", "askAIAnything": "Ask AI anything", "askAIFor": "Ask AI", - "noResultForSearching": "No result for \"{}\"", - "noResultForSearchingHint": "Some results may be in your deleted pages", + "noResultForSearching": "No matches found", + "noResultForSearchingHint": "Try different questions or keywords.\n Some pages may be in the Trash.", "bestMatch": "Best match", "placeholder": { "actions": "Search actions..."