fix: some mobile search LR issues (#7836)

This commit is contained in:
Morn 2025-04-28 11:16:02 +08:00 committed by GitHub
parent 1f54946a0e
commit 15bfdfb550
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 120 additions and 90 deletions

View File

@ -36,7 +36,7 @@ void main() {
expect(find.byType(MobileSearchRecentList), findsNothing); expect(find.byType(MobileSearchRecentList), findsNothing);
expect(find.byType(MobileSearchResultList), findsOneWidget); expect(find.byType(MobileSearchResultList), findsOneWidget);
expect( expect(
find.text(LocaleKeys.search_noResultForSearching.tr(args: [query])), find.text(LocaleKeys.search_noResultForSearching.tr()),
findsOneWidget, findsOneWidget,
); );

View File

@ -47,9 +47,10 @@ class NotificationReminderBloc
status: NotificationReminderStatus.error, status: NotificationReminderStatus.error,
), ),
); );
return;
} }
final layout = view!.layout; final layout = view.layout;
if (layout.isDocumentView) { if (layout.isDocumentView) {
final node = await _getContent(reminder); final node = await _getContent(reminder);

View File

@ -163,6 +163,7 @@ class _MobileBottomSheetEditLinkWidgetState
decoration: LinkStyle.buildLinkTextFieldInputDecoration( decoration: LinkStyle.buildLinkTextFieldInputDecoration(
LocaleKeys.document_toolbar_linkNameHint.tr(), LocaleKeys.document_toolbar_linkNameHint.tr(),
contentPadding: EdgeInsets.all(14), contentPadding: EdgeInsets.all(14),
radius: 12,
context, context,
), ),
), ),

View File

@ -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/command_palette_bloc.dart';
import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart'; import 'package:appflowy/workspace/application/command_palette/search_result_ext.dart';
import 'package:appflowy/workspace/application/view/view_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-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart'; import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:appflowy_ui/appflowy_ui.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:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -28,7 +25,7 @@ class MobileSearchResultCell extends StatelessWidget {
child: Row( child: Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
buildIcon(), buildIcon(theme),
HSpace(12), HSpace(12),
Flexible( Flexible(
child: Column( child: Column(
@ -57,36 +54,17 @@ class MobileSearchResultCell extends StatelessWidget {
); );
} }
Widget buildIcon() { Widget buildIcon(AppFlowyThemeData theme) {
final icon = item.icon; final icon = item.icon;
if (icon.ty == ResultIconTypePB.Emoji) { if (icon.ty == ResultIconTypePB.Emoji) {
return icon.getIcon(size: 16.0, lineHeight: 20 / 16) ?? SizedBox.shrink();
} else {
return icon.getIcon(size: 20) ?? SizedBox.shrink(); 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) { 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}',
style: theme.textStyle.body
.standard(color: theme.textColorScheme.secondary),
),
],
);
} else {
return BlocProvider( return BlocProvider(
create: (context) => ViewAncestorBloc(item.id), create: (context) => ViewAncestorBloc(item.id),
child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>( child: BlocBuilder<ViewAncestorBloc, ViewAncestorState>(
@ -105,7 +83,6 @@ class MobileSearchResultCell extends StatelessWidget {
), ),
); );
} }
}
Widget buildSummary(AppFlowyThemeData theme) { Widget buildSummary(AppFlowyThemeData theme) {
if (item.content.isEmpty) { if (item.content.isEmpty) {

View File

@ -61,24 +61,19 @@ class MobileSearchScreen extends StatelessWidget {
), ),
), ),
); );
return Scaffold( return Provider.value(
body: SafeArea(
bottom: false,
child: Provider.value(
value: userProfile, value: userProfile,
child: MobileSearchPage( child: MobileSearchPage(
userProfile: userProfile, userProfile: userProfile,
workspaceLatestPB: latest, workspaceLatestPB: latest,
), ),
),
),
); );
}, },
); );
} }
} }
class MobileSearchPage extends StatelessWidget { class MobileSearchPage extends StatefulWidget {
const MobileSearchPage({ const MobileSearchPage({
super.key, super.key,
required this.userProfile, required this.userProfile,
@ -88,32 +83,63 @@ class MobileSearchPage extends StatelessWidget {
final UserProfilePB userProfile; final UserProfilePB userProfile;
final WorkspaceLatestPB workspaceLatestPB; final WorkspaceLatestPB workspaceLatestPB;
@override
State<MobileSearchPage> createState() => _MobileSearchPageState();
}
class _MobileSearchPageState extends State<MobileSearchPage> {
bool get enableShowAISearch => bool get enableShowAISearch =>
userProfile.workspaceType == WorkspaceTypePB.ServerW; widget.userProfile.workspaceType == WorkspaceTypePB.ServerW;
final focusNode = FocusNode();
@override
void dispose() {
focusNode.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocBuilder<CommandPaletteBloc, CommandPaletteState>( return BlocBuilder<CommandPaletteBloc, CommandPaletteState>(
builder: (context, state) { builder: (context, state) {
return Padding( return SafeArea(
child: Scaffold(
body: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16), padding: const EdgeInsets.symmetric(horizontal: 16),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
MobileSearchTextfield( MobileSearchTextfield(
focusNode: focusNode,
hintText: enableShowAISearch hintText: enableShowAISearch
? LocaleKeys.search_searchOrAskAI.tr() ? LocaleKeys.search_searchOrAskAI.tr()
: LocaleKeys.search_label.tr(), : LocaleKeys.search_label.tr(),
query: state.query ?? '', query: state.query ?? '',
onChanged: (value) => context onChanged: (value) =>
.read<CommandPaletteBloc>() context.read<CommandPaletteBloc>().add(
.add(CommandPaletteEvent.searchChanged(search: value)), CommandPaletteEvent.searchChanged(search: value),
),
), ),
if (enableShowAISearch) if (enableShowAISearch)
MobileSearchAskAiEntrance(query: state.query), MobileSearchAskAiEntrance(query: state.query),
Flexible(child: SafeArea(child: MobileSearchResult())), Flexible(
child: NotificationListener(
child: MobileSearchResult(),
onNotification: (t) {
if (t is ScrollUpdateNotification) {
if (focusNode.hasFocus) {
focusNode.unfocus();
}
}
return true;
},
),
),
], ],
), ),
),
),
); );
}, },
); );

View File

@ -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/command_palette/search_result_list_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart'; import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/view/view_service.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_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart'; import 'package:appflowy_result/appflowy_result.dart';
import 'package:appflowy_ui/appflowy_ui.dart'; import 'package:appflowy_ui/appflowy_ui.dart';
@ -36,13 +37,18 @@ class MobileSearchRecentList extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
final commandPaletteState = context.read<CommandPaletteBloc>().state;
final trashIdSet = commandPaletteState.trash.map((e) => e.id).toSet();
return BlocProvider( return BlocProvider(
create: (context) => create: (context) =>
RecentViewsBloc()..add(const RecentViewsEvent.initial()), RecentViewsBloc()..add(const RecentViewsEvent.initial()),
child: BlocBuilder<RecentViewsBloc, RecentViewsState>( child: BlocBuilder<RecentViewsBloc, RecentViewsState>(
builder: (context, state) { builder: (context, state) {
final List<ViewPB> recentViews = final List<ViewPB> recentViews = state.views
state.views.map((e) => e.item).toSet().toList(); .map((e) => e.item)
.where((e) => !trashIdSet.contains(e.id))
.toList();
return SingleChildScrollView( return SingleChildScrollView(
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -115,7 +121,7 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
children: [ children: [
const VSpace(16), const VSpace(16),
Text( Text(
LocaleKeys.search_bestMatch.tr(), LocaleKeys.commandPalette_bestMatches.tr(),
style: theme.textStyle.body style: theme.textStyle.body
.enhanced(color: theme.textColorScheme.secondary), .enhanced(color: theme.textColorScheme.secondary),
), ),
@ -130,6 +136,10 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
.fold((s) => s, (s) => null); .fold((s) => s, (s) => null);
if (view != null && context.mounted) { if (view != null && context.mounted) {
await _goToView(context, view); await _goToView(context, view);
} else {
Log.error(
'tapping search result, view not found: ${item.id}',
);
} }
}, },
child: MobileSearchResultCell( child: MobileSearchResultCell(
@ -161,12 +171,13 @@ class _MobileSearchResultListState extends State<MobileSearchResultList> {
), ),
const VSpace(12), const VSpace(12),
Text( Text(
LocaleKeys.search_noResultForSearching.tr(args: [query]), LocaleKeys.search_noResultForSearching.tr(),
style: theme.textStyle.body.enhanced(color: textColor), style: theme.textStyle.body.enhanced(color: textColor),
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
Text( Text(
textAlign: TextAlign.center,
LocaleKeys.search_noResultForSearchingHint.tr(), LocaleKeys.search_noResultForSearchingHint.tr(),
style: theme.textStyle.caption.standard(color: textColor), style: theme.textStyle.caption.standard(color: textColor),
), ),

View File

@ -11,11 +11,13 @@ class MobileSearchTextfield extends StatefulWidget {
this.onChanged, this.onChanged,
required this.hintText, required this.hintText,
required this.query, required this.query,
required this.focusNode,
}); });
final String hintText; final String hintText;
final String query; final String query;
final ValueChanged<String>? onChanged; final ValueChanged<String>? onChanged;
final FocusNode focusNode;
@override @override
State<MobileSearchTextfield> createState() => _MobileSearchTextfieldState(); State<MobileSearchTextfield> createState() => _MobileSearchTextfieldState();
@ -23,18 +25,15 @@ class MobileSearchTextfield extends StatefulWidget {
class _MobileSearchTextfieldState extends State<MobileSearchTextfield> { class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
late final TextEditingController controller; late final TextEditingController controller;
late final FocusNode focusNode;
final ValueNotifier<bool> hasFocusValueNotifier = ValueNotifier(true); final ValueNotifier<bool> hasFocusValueNotifier = ValueNotifier(true);
FocusNode get focusNode => widget.focusNode;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
controller = TextEditingController(text: widget.query); controller = TextEditingController(text: widget.query);
focusNode = FocusNode(); focusNode.addListener(onFocusChanged);
focusNode.addListener(() {
if (!mounted) return;
hasFocusValueNotifier.value = focusNode.hasFocus;
});
controller.addListener(() { controller.addListener(() {
if (!mounted) return; if (!mounted) return;
widget.onChanged?.call(controller.text); widget.onChanged?.call(controller.text);
@ -46,7 +45,7 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
@override @override
void dispose() { void dispose() {
controller.dispose(); controller.dispose();
focusNode.dispose(); focusNode.removeListener(onFocusChanged);
hasFocusValueNotifier.dispose(); hasFocusValueNotifier.dispose();
bottomNavigationBarItemType.removeListener(onBackOrLeave); bottomNavigationBarItemType.removeListener(onBackOrLeave);
super.dispose(); super.dispose();
@ -157,11 +156,17 @@ class _MobileSearchTextfieldState extends State<MobileSearchTextfield> {
); );
} }
void onFocusChanged() {
if (!mounted) return;
hasFocusValueNotifier.value = focusNode.hasFocus;
}
void onBackOrLeave() { void onBackOrLeave() {
final label = bottomNavigationBarItemType.value; final label = bottomNavigationBarItemType.value;
if (label == BottomNavigationBarItemType.search.label) { if (label == BottomNavigationBarItemType.search.label) {
focusNode.requestFocus(); focusNode.requestFocus();
} else { } else {
focusNode.unfocus();
controller.clear(); controller.clear();
} }
} }

View File

@ -11,10 +11,11 @@ class LinkStyle {
BuildContext context, { BuildContext context, {
bool showErrorBorder = false, bool showErrorBorder = false,
EdgeInsets? contentPadding, EdgeInsets? contentPadding,
double? radius,
}) { }) {
final theme = AppFlowyTheme.of(context); final theme = AppFlowyTheme.of(context);
final border = OutlineInputBorder( final border = OutlineInputBorder(
borderRadius: BorderRadius.all(Radius.circular(8.0)), borderRadius: BorderRadius.all(Radius.circular(radius ?? 8.0)),
borderSide: BorderSide(color: borderColor(context)), borderSide: BorderSide(color: borderColor(context)),
); );
final enableBorder = border.copyWith( final enableBorder = border.copyWith(

View File

@ -5,7 +5,11 @@ import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
extension GetIcon on ResultIconPB { 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; final iconValue = value, iconType = ty;
if (iconType == ResultIconTypePB.Emoji) { if (iconType == ResultIconTypePB.Emoji) {
return iconValue.isNotEmpty return iconValue.isNotEmpty
@ -17,7 +21,11 @@ extension GetIcon on ResultIconPB {
: null; : null;
} else if (ty == ResultIconTypePB.Icon) { } else if (ty == ResultIconTypePB.Icon) {
if (_resultIconValueTypes.contains(iconValue)) { if (_resultIconValueTypes.contains(iconValue)) {
return FlowySvg(getViewSvg(), size: Size.square(size)); return FlowySvg(
getViewSvg(),
size: Size.square(size),
color: iconColor,
);
} }
return RawEmojiIconWidget( return RawEmojiIconWidget(
emoji: EmojiIconData(iconType.toFlowyIconType(), iconValue), emoji: EmojiIconData(iconType.toFlowyIconType(), iconValue),

View File

@ -2347,8 +2347,8 @@
"searchOrAskAI": "Search or ask AI", "searchOrAskAI": "Search or ask AI",
"askAIAnything": "Ask AI anything", "askAIAnything": "Ask AI anything",
"askAIFor": "Ask AI", "askAIFor": "Ask AI",
"noResultForSearching": "No result for \"{}\"", "noResultForSearching": "No matches found",
"noResultForSearchingHint": "Some results may be in your deleted pages", "noResultForSearchingHint": "Try different questions or keywords.\n Some pages may be in the Trash.",
"bestMatch": "Best match", "bestMatch": "Best match",
"placeholder": { "placeholder": {
"actions": "Search actions..." "actions": "Search actions..."