chore: adjust ui

This commit is contained in:
Nathan 2025-04-15 22:19:16 +08:00
parent 9291236733
commit 846172a709
5 changed files with 96 additions and 29 deletions

View File

@ -25,6 +25,7 @@ class SearchResultListBloc
state.copyWith( state.copyWith(
hoveredSummary: event.summary, hoveredSummary: event.summary,
hoveredResult: null, hoveredResult: null,
userHovered: event.userHovered,
openPageId: null, openPageId: null,
), ),
); );
@ -38,6 +39,7 @@ class SearchResultListBloc
state.copyWith( state.copyWith(
hoveredSummary: null, hoveredSummary: null,
hoveredResult: event.item, hoveredResult: event.item,
userHovered: event.userHovered,
openPageId: null, openPageId: null,
), ),
); );
@ -55,9 +57,11 @@ class SearchResultListBloc
class SearchResultListEvent with _$SearchResultListEvent { class SearchResultListEvent with _$SearchResultListEvent {
const factory SearchResultListEvent.onHoverSummary({ const factory SearchResultListEvent.onHoverSummary({
required SearchSummaryPB summary, required SearchSummaryPB summary,
required bool userHovered,
}) = _OnHoverSummary; }) = _OnHoverSummary;
const factory SearchResultListEvent.onHoverResult({ const factory SearchResultListEvent.onHoverResult({
required SearchResultItem item, required SearchResultItem item,
required bool userHovered,
}) = _OnHoverResult; }) = _OnHoverResult;
const factory SearchResultListEvent.openPage({ const factory SearchResultListEvent.openPage({
@ -72,6 +76,7 @@ class SearchResultListState with _$SearchResultListState {
@Default(null) SearchSummaryPB? hoveredSummary, @Default(null) SearchSummaryPB? hoveredSummary,
@Default(null) SearchResultItem? hoveredResult, @Default(null) SearchResultItem? hoveredResult,
@Default(null) String? openPageId, @Default(null) String? openPageId,
@Default(false) bool userHovered,
}) = _SearchResultListState; }) = _SearchResultListState;
factory SearchResultListState.initial() => const SearchResultListState(); factory SearchResultListState.initial() => const SearchResultListState();

View File

@ -17,10 +17,12 @@ class SearchResultCell extends StatefulWidget {
super.key, super.key,
required this.item, required this.item,
this.isTrashed = false, this.isTrashed = false,
this.isHovered = false,
}); });
final SearchResultItem item; final SearchResultItem item;
final bool isTrashed; final bool isTrashed;
final bool isHovered;
@override @override
State<SearchResultCell> createState() => _SearchResultCellState(); State<SearchResultCell> createState() => _SearchResultCellState();
@ -142,7 +144,10 @@ class _SearchResultCellState extends State<SearchResultCell> {
onFocusChange: (hasFocus) { onFocusChange: (hasFocus) {
setState(() { setState(() {
context.read<SearchResultListBloc>().add( context.read<SearchResultListBloc>().add(
SearchResultListEvent.onHoverResult(item: widget.item), SearchResultListEvent.onHoverResult(
item: widget.item,
userHovered: true,
),
); );
_hasFocus = hasFocus; _hasFocus = hasFocus;
}); });
@ -150,10 +155,13 @@ class _SearchResultCellState extends State<SearchResultCell> {
child: FlowyHover( child: FlowyHover(
onHover: (value) { onHover: (value) {
context.read<SearchResultListBloc>().add( context.read<SearchResultListBloc>().add(
SearchResultListEvent.onHoverResult(item: widget.item), SearchResultListEvent.onHoverResult(
item: widget.item,
userHovered: true,
),
); );
}, },
isSelected: () => _hasFocus, isSelected: () => _hasFocus || widget.isHovered,
style: HoverStyle( style: HoverStyle(
borderRadius: BorderRadius.circular(8), borderRadius: BorderRadius.circular(8),
hoverColor: hoverColor:

View File

@ -16,7 +16,7 @@ import 'package:flutter_animate/flutter_animate.dart';
import 'search_result_cell.dart'; import 'search_result_cell.dart';
import 'search_summary_cell.dart'; import 'search_summary_cell.dart';
class SearchResultList extends StatelessWidget { class SearchResultList extends StatefulWidget {
const SearchResultList({ const SearchResultList({
required this.trash, required this.trash,
required this.resultItems, required this.resultItems,
@ -27,6 +27,26 @@ class SearchResultList extends StatelessWidget {
final List<TrashPB> trash; final List<TrashPB> trash;
final List<SearchResultItem> resultItems; final List<SearchResultItem> resultItems;
final List<SearchSummaryPB> resultSummaries; final List<SearchSummaryPB> resultSummaries;
@override
State<SearchResultList> createState() => _SearchResultListState();
}
class _SearchResultListState extends State<SearchResultList> {
late final SearchResultListBloc bloc;
@override
void initState() {
super.initState();
bloc = SearchResultListBloc();
}
@override
void dispose() {
bloc.close();
super.dispose();
}
Widget _buildSectionHeader(String title) => Padding( Widget _buildSectionHeader(String title) => Padding(
padding: const EdgeInsets.symmetric(vertical: 8) + padding: const EdgeInsets.symmetric(vertical: 8) +
const EdgeInsets.only(left: 8), const EdgeInsets.only(left: 8),
@ -48,7 +68,21 @@ class SearchResultList extends StatelessWidget {
], ],
); );
} }
if (resultSummaries.isNotEmpty) {
if (widget.resultSummaries.isNotEmpty) {
if (!bloc.state.userHovered) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
bloc.add(
SearchResultListEvent.onHoverSummary(
summary: widget.resultSummaries[0],
userHovered: false,
),
);
},
);
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -56,10 +90,11 @@ class SearchResultList extends StatelessWidget {
ListView.separated( ListView.separated(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
itemCount: resultSummaries.length, itemCount: widget.resultSummaries.length,
separatorBuilder: (_, __) => const Divider(height: 0), separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (_, index) => SearchSummaryCell( itemBuilder: (_, index) => SearchSummaryCell(
summary: resultSummaries[index], summary: widget.resultSummaries[index],
isHovered: bloc.state.hoveredSummary != null,
), ),
), ),
], ],
@ -78,13 +113,14 @@ class SearchResultList extends StatelessWidget {
ListView.separated( ListView.separated(
physics: const NeverScrollableScrollPhysics(), physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true, shrinkWrap: true,
itemCount: resultItems.length, itemCount: widget.resultItems.length,
separatorBuilder: (_, __) => const Divider(height: 0), separatorBuilder: (_, __) => const Divider(height: 0),
itemBuilder: (_, index) { itemBuilder: (_, index) {
final item = resultItems[index]; final item = widget.resultItems[index];
return SearchResultCell( return SearchResultCell(
item: item, item: item,
isTrashed: trash.any((t) => t.id == item.id), isTrashed: widget.trash.any((t) => t.id == item.id),
isHovered: bloc.state.hoveredResult?.id == item.id,
); );
}, },
), ),
@ -96,11 +132,9 @@ class SearchResultList extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6), padding: const EdgeInsets.symmetric(horizontal: 6),
child: BlocProvider( child: BlocProvider.value(
create: (context) => SearchResultListBloc(), value: bloc,
child: BlocListener<SearchResultListBloc, SearchResultListState>( child: BlocListener<SearchResultListBloc, SearchResultListState>(
listenWhen: (previous, current) =>
previous.openPageId != current.openPageId,
listener: (context, state) { listener: (context, state) {
if (state.openPageId != null) { if (state.openPageId != null) {
FlowyOverlay.pop(context); FlowyOverlay.pop(context);
@ -116,18 +150,27 @@ class SearchResultList extends StatelessWidget {
children: [ children: [
Flexible( Flexible(
flex: 7, flex: 7,
child: ListView( child: BlocBuilder<SearchResultListBloc, SearchResultListState>(
shrinkWrap: true, buildWhen: (previous, current) =>
physics: const ClampingScrollPhysics(), previous.hoveredResult != current.hoveredResult ||
children: [ previous.hoveredSummary != current.hoveredSummary,
_buildAIOverviewSection(context), builder: (context, state) {
const VSpace(10), return ListView(
if (resultItems.isNotEmpty) _buildResultsSection(context), shrinkWrap: true,
], physics: const ClampingScrollPhysics(),
children: [
_buildAIOverviewSection(context),
const VSpace(10),
if (widget.resultItems.isNotEmpty)
_buildResultsSection(context),
],
);
},
), ),
), ),
const HSpace(10), const HSpace(10),
if (resultItems.any((item) => item.content.isNotEmpty)) ...[ if (widget.resultItems
.any((item) => item.content.isNotEmpty)) ...[
const VerticalDivider( const VerticalDivider(
thickness: 1.0, thickness: 1.0,
), ),

View File

@ -14,17 +14,23 @@ import 'package:flutter_bloc/flutter_bloc.dart';
class SearchSummaryCell extends StatelessWidget { class SearchSummaryCell extends StatelessWidget {
const SearchSummaryCell({ const SearchSummaryCell({
required this.summary, required this.summary,
required this.isHovered,
super.key, super.key,
}); });
final SearchSummaryPB summary; final SearchSummaryPB summary;
final bool isHovered;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return FlowyHover( return FlowyHover(
isSelected: () => isHovered,
onHover: (value) { onHover: (value) {
context.read<SearchResultListBloc>().add( context.read<SearchResultListBloc>().add(
SearchResultListEvent.onHoverSummary(summary: summary), SearchResultListEvent.onHoverSummary(
summary: summary,
userHovered: true,
),
); );
}, },
style: HoverStyle( style: HoverStyle(

View File

@ -453,7 +453,6 @@ impl AIManager {
if let Some(local_model) = self.local_ai.get_plugin_chat_model() { if let Some(local_model) = self.local_ai.get_plugin_chat_model() {
let model = AIModel::local(local_model, "".to_string()); let model = AIModel::local(local_model, "".to_string());
current_active_local_ai_model = Some(model.clone()); current_active_local_ai_model = Some(model.clone());
trace!("[Model Selection] current local ai model: {}", model.name); trace!("[Model Selection] current local ai model: {}", model.name);
models.push(model); models.push(model);
} }
@ -466,7 +465,7 @@ impl AIManager {
} }
// Global active model is the model selected by the user in the workspace settings. // Global active model is the model selected by the user in the workspace settings.
let server_active_model = self let mut server_active_model = self
.get_workspace_select_model() .get_workspace_select_model()
.await .await
.map(|m| AIModel::server(m, "".to_string())) .map(|m| AIModel::server(m, "".to_string()))
@ -478,10 +477,14 @@ impl AIManager {
); );
let mut user_selected_model = server_active_model.clone(); let mut user_selected_model = server_active_model.clone();
// when current select model is deprecated, reset the model to default
if !models.iter().any(|m| m.name == server_active_model.name) {
server_active_model = AIModel::default();
}
let source_key = ai_available_models_key(&source); let source_key = ai_available_models_key(&source);
// If source is provided, try to get the user-selected model from the store. User selected // We use source to identify user selected model. source can be document id or chat id.
// model will be used as the active model if it exists.
match self.store_preferences.get_object::<AIModel>(&source_key) { match self.store_preferences.get_object::<AIModel>(&source_key) {
None => { None => {
// when there is selected model and current local ai is active, then use local ai // when there is selected model and current local ai is active, then use local ai
@ -491,6 +494,8 @@ impl AIManager {
}, },
Some(mut model) => { Some(mut model) => {
trace!("[Model Selection] user previous select model: {:?}", model); trace!("[Model Selection] user previous select model: {:?}", model);
// If source is provided, try to get the user-selected model from the store. User selected
// model will be used as the active model if it exists.
if model.is_local { if model.is_local {
if let Some(local_ai_model) = &current_active_local_ai_model { if let Some(local_ai_model) = &current_active_local_ai_model {
if local_ai_model.name != model.name { if local_ai_model.name != model.name {
@ -508,7 +513,7 @@ impl AIManager {
.iter() .iter()
.find(|m| m.name == user_selected_model.name) .find(|m| m.name == user_selected_model.name)
.cloned() .cloned()
.or(Some(server_active_model)); .or(Some(server_active_model.clone()));
// Update the stored preference if a different model is used. // Update the stored preference if a different model is used.
if let Some(ref active_model) = active_model { if let Some(ref active_model) = active_model {