chore: polish ui (#5883)

* chore: polish ui

* chore: clippy

* chore: fmt
This commit is contained in:
Nathan.fooo
2024-08-06 13:17:56 +08:00
committed by GitHub
parent 453e6309d5
commit 5757cc9a1d
9 changed files with 124 additions and 72 deletions

View File

@ -175,7 +175,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425 image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@ -197,4 +197,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.15.2 COCOAPODS: 1.11.3

View File

@ -2,10 +2,12 @@ import 'dart:async';
import 'package:appflowy/workspace/application/view/view_ext.dart'; import 'package:appflowy/workspace/application/view/view_ext.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:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'chat_input_action_control.dart'; import 'chat_input_action_control.dart';
@ -43,34 +45,28 @@ class ChatInputActionBloc
); );
}, },
refreshViews: (List<ViewPB> views) { refreshViews: (List<ViewPB> views) {
final List<ViewActionPage> pages = _filterPages(
views,
state.selectedPages,
state.filter,
);
emit( emit(
state.copyWith( state.copyWith(
views: views, views: views,
pages: views.map((v) => ViewActionPage(view: v)).toList(), pages: pages,
indicator: const ChatActionMenuIndicator.ready(),
), ),
); );
}, },
filter: (String filter) { filter: (String filter) {
final List<ViewActionPage> pages = []; Log.debug("Filter chat input pages: $filter");
if (filter.isEmpty) { final List<ViewActionPage> pages = _filterPages(
pages.addAll(state.views.map((v) => ViewActionPage(view: v))); state.views,
} else { state.selectedPages,
pages.addAll( filter,
state.views
.where(
(v) => v.name.toLowerCase().contains(
filter.toLowerCase(),
),
)
.map(
(v) => ViewActionPage(view: v),
),
); );
}
pages.retainWhere((view) { emit(state.copyWith(pages: pages, filter: filter));
return !state.selectedPages.contains(view);
});
emit(state.copyWith(pages: pages));
}, },
handleKeyEvent: (PhysicalKeyboardKey physicalKey) { handleKeyEvent: (PhysicalKeyboardKey physicalKey) {
emit( emit(
@ -81,8 +77,14 @@ class ChatInputActionBloc
}, },
addPage: (ChatInputActionPage page) { addPage: (ChatInputActionPage page) {
if (!state.selectedPages.any((p) => p.pageId == page.pageId)) { if (!state.selectedPages.any((p) => p.pageId == page.pageId)) {
final List<ViewActionPage> pages = _filterPages(
state.views,
state.selectedPages,
state.filter,
);
emit( emit(
state.copyWith( state.copyWith(
pages: pages,
selectedPages: [...state.selectedPages, page], selectedPages: [...state.selectedPages, page],
), ),
); );
@ -93,9 +95,11 @@ class ChatInputActionBloc
List.from(state.selectedPages); List.from(state.selectedPages);
selectedPages.retainWhere((t) => !text.contains(t.title)); selectedPages.retainWhere((t) => !text.contains(t.title));
final allPages = final List<ViewActionPage> allPages = _filterPages(
state.views.map((v) => ViewActionPage(view: v)).toList(); state.views,
allPages.retainWhere((view) => !selectedPages.contains(view)); state.selectedPages,
state.filter,
);
emit( emit(
state.copyWith( state.copyWith(
@ -108,6 +112,32 @@ class ChatInputActionBloc
} }
} }
List<ViewActionPage> _filterPages(
List<ViewPB> views,
List<ChatInputActionPage> selectedPages,
String filter,
) {
final pages = views
.map(
(v) => ViewActionPage(view: v),
)
.toList();
pages.retainWhere((page) {
return !selectedPages.contains(page);
});
if (filter.isEmpty) {
return pages;
}
return pages
.where(
(v) => v.title.toLowerCase().contains(filter.toLowerCase()),
)
.toList();
}
class ViewActionPage extends ChatInputActionPage { class ViewActionPage extends ChatInputActionPage {
ViewActionPage({required this.view}); ViewActionPage({required this.view});
@ -124,6 +154,9 @@ class ViewActionPage extends ChatInputActionPage {
@override @override
dynamic get page => view; dynamic get page => view;
@override
Widget get icon => view.defaultIcon();
} }
@freezed @freezed
@ -146,7 +179,10 @@ class ChatInputActionState with _$ChatInputActionState {
@Default([]) List<ViewPB> views, @Default([]) List<ViewPB> views,
@Default([]) List<ChatInputActionPage> pages, @Default([]) List<ChatInputActionPage> pages,
@Default([]) List<ChatInputActionPage> selectedPages, @Default([]) List<ChatInputActionPage> selectedPages,
@Default("") String filter,
ChatInputKeyboardEvent? keyboardKey, ChatInputKeyboardEvent? keyboardKey,
@Default(ChatActionMenuIndicator.loading())
ChatActionMenuIndicator indicator,
}) = _ChatInputActionState; }) = _ChatInputActionState;
} }
@ -159,3 +195,9 @@ class ChatInputKeyboardEvent extends Equatable {
@override @override
List<Object?> get props => [timestamp]; List<Object?> get props => [timestamp];
} }
@freezed
class ChatActionMenuIndicator with _$ChatActionMenuIndicator {
const factory ChatActionMenuIndicator.ready() = _Ready;
const factory ChatActionMenuIndicator.loading() = _Loading;
}

View File

@ -9,6 +9,7 @@ abstract class ChatInputActionPage extends Equatable {
String get title; String get title;
String get pageId; String get pageId;
dynamic get page; dynamic get page;
Widget get icon;
} }
typedef ChatInputMetadata = Map<String, ChatInputActionPage>; typedef ChatInputMetadata = Map<String, ChatInputActionPage>;
@ -26,10 +27,9 @@ class ChatInputActionControl extends ChatActionHandler {
final String chatId; final String chatId;
// Private attributes // Private attributes
bool _isShowActionMenu = false;
String _atText = ""; String _atText = "";
String _prevText = ""; String _prevText = "";
bool _didLoadViews = false; String _showMenuText = "";
// Getter // Getter
List<String> get tags => List<String> get tags =>
@ -43,12 +43,12 @@ class ChatInputActionControl extends ChatActionHandler {
void handleKeyEvent(KeyEvent event) { void handleKeyEvent(KeyEvent event) {
// ignore: deprecated_member_use // ignore: deprecated_member_use
if (event is KeyDownEvent || event is RawKeyDownEvent) { if (event is KeyDownEvent || event is RawKeyDownEvent) {
commandBloc.add(ChatInputActionEvent.handleKeyEvent(event.physicalKey)); _commandBloc.add(ChatInputActionEvent.handleKeyEvent(event.physicalKey));
} }
} }
bool canHandleKeyEvent(KeyEvent event) { bool canHandleKeyEvent(KeyEvent event) {
return _isShowActionMenu && return _showMenuText.isNotEmpty &&
<PhysicalKeyboardKey>{ <PhysicalKeyboardKey>{
PhysicalKeyboardKey.arrowDown, PhysicalKeyboardKey.arrowDown,
PhysicalKeyboardKey.arrowUp, PhysicalKeyboardKey.arrowUp,
@ -58,35 +58,29 @@ class ChatInputActionControl extends ChatActionHandler {
} }
void dispose() { void dispose() {
commandBloc.close(); _commandBloc.close();
} }
@override @override
void onSelected(ChatInputActionPage page) { void onSelected(ChatInputActionPage page) {
_atText = "";
_isShowActionMenu = false;
_commandBloc.add(ChatInputActionEvent.addPage(page)); _commandBloc.add(ChatInputActionEvent.addPage(page));
textController.text = textController.text = "$_showMenuText${page.title}";
"${textController.text.replaceAll(_atText, '')}${page.title}";
_prevText = textController.text; onExit();
} }
@override @override
void onExit() { void onExit() {
_atText = ""; _atText = "";
_isShowActionMenu = false; _showMenuText = "";
_didLoadViews = false; _prevText = "";
commandBloc.add(const ChatInputActionEvent.filter("")); _commandBloc.add(const ChatInputActionEvent.filter(""));
} }
@override @override
void onEnter() { void onEnter() {
if (!_didLoadViews) { _commandBloc.add(const ChatInputActionEvent.started());
_didLoadViews = true; _showMenuText = textController.text;
commandBloc.add(const ChatInputActionEvent.started());
}
_isShowActionMenu = true;
} }
@override @override
@ -134,16 +128,15 @@ class ChatInputActionControl extends ChatActionHandler {
} }
// If the action menu is shown, filter the views // If the action menu is shown, filter the views
if (_isShowActionMenu) { if (_showMenuText.isNotEmpty) {
// before filter the views, remove the first character '@' if it exists if (text.length >= _showMenuText.length) {
if (inputText.startsWith("@")) { final filterText = inputText.substring(_showMenuText.length);
final filter = inputText.substring(1); _commandBloc.add(ChatInputActionEvent.filter(filterText));
commandBloc.add(ChatInputActionEvent.filter(filter));
} }
// If the text change from "xxx @"" to "xxx", which means user delete the @, we should hide the action menu // If the text change from "xxx @"" to "xxx", which means user delete the @, we should hide the action menu
if (_atText.isNotEmpty && !inputText.contains(_atText)) { if (_atText.isNotEmpty && !inputText.contains(_atText)) {
commandBloc.add( _commandBloc.add(
const ChatInputActionEvent.handleKeyEvent(PhysicalKeyboardKey.escape), const ChatInputActionEvent.handleKeyEvent(PhysicalKeyboardKey.escape),
); );
} }

View File

@ -489,6 +489,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
child: BlocBuilder<ChatInputStateBloc, ChatInputStateState>( child: BlocBuilder<ChatInputStateBloc, ChatInputStateState>(
builder: (context, state) { builder: (context, state) {
// Show different hint text based on the AI type // Show different hint text based on the AI type
final aiType = state.aiType;
final hintText = state.aiType.when( final hintText = state.aiType.when(
appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(), appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(), localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
@ -500,6 +501,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
selector: (state) => state.streamingStatus, selector: (state) => state.streamingStatus,
builder: (context, state) { builder: (context, state) {
return ChatInput( return ChatInput(
aiType: aiType,
chatId: widget.view.id, chatId: widget.view.id,
onSendPressed: (message) { onSendPressed: (message) {
context.read<ChatBloc>().add( context.read<ChatBloc>().add(

View File

@ -1,3 +1,4 @@
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart'; import 'package:appflowy/plugins/ai_chat/presentation/chat_input_action_menu.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart'; import 'package:appflowy/plugins/ai_chat/application/chat_input_action_control.dart';
import 'package:extended_text_field/extended_text_field.dart'; import 'package:extended_text_field/extended_text_field.dart';
@ -26,6 +27,7 @@ class ChatInput extends StatefulWidget {
required this.isStreaming, required this.isStreaming,
required this.onStopStreaming, required this.onStopStreaming,
required this.hintText, required this.hintText,
required this.aiType,
}); });
final bool? isAttachmentUploading; final bool? isAttachmentUploading;
@ -36,6 +38,7 @@ class ChatInput extends StatefulWidget {
final String chatId; final String chatId;
final bool isStreaming; final bool isStreaming;
final String hintText; final String hintText;
final AIType aiType;
@override @override
State<ChatInput> createState() => _ChatInputState(); State<ChatInput> createState() => _ChatInputState();
@ -117,7 +120,9 @@ class _ChatInputState extends State<ChatInput> {
Expanded(child: _inputTextField(textPadding)), Expanded(child: _inputTextField(textPadding)),
// TODO(lucas): support mobile // TODO(lucas): support mobile
if (PlatformExtension.isDesktop) _atButton(buttonPadding), if (PlatformExtension.isDesktop &&
widget.aiType == const AIType.appflowyAI())
_atButton(buttonPadding),
_sendButton(buttonPadding), _sendButton(buttonPadding),
const HSpace(14), const HSpace(14),
], ],
@ -169,6 +174,7 @@ class _ChatInputState extends State<ChatInput> {
decoration: InputDecoration( decoration: InputDecoration(
border: InputBorder.none, border: InputBorder.none,
hintText: widget.hintText, hintText: widget.hintText,
focusedBorder: InputBorder.none,
hintStyle: TextStyle( hintStyle: TextStyle(
color: AFThemeExtension.of(context).textColor.withOpacity(0.5), color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
), ),
@ -190,6 +196,10 @@ class _ChatInputState extends State<ChatInput> {
} }
void _handleOnTextChange(BuildContext context, String text) { void _handleOnTextChange(BuildContext context, String text) {
if (widget.aiType != const AIType.appflowyAI()) {
return;
}
if (PlatformExtension.isDesktop) { if (PlatformExtension.isDesktop) {
if (_inputActionControl.onTextChanged(text)) { if (_inputActionControl.onTextChanged(text)) {
ChatActionsMenu( ChatActionsMenu(

View File

@ -78,6 +78,8 @@ class ChatActionsMenu {
), ),
maxHeight, maxHeight,
); );
final isLoading =
state.indicator == const ChatActionMenuIndicator.loading();
return Stack( return Stack(
children: [ children: [
@ -106,6 +108,7 @@ class ChatActionsMenu {
vertical: 2, vertical: 2,
), ),
child: ActionList( child: ActionList(
isLoading: isLoading,
handler: handler, handler: handler,
onDismiss: () => dismiss(), onDismiss: () => dismiss(),
pages: state.pages, pages: state.pages,
@ -149,6 +152,7 @@ class _ActionItem extends StatelessWidget {
borderRadius: BorderRadius.circular(4.0), borderRadius: BorderRadius.circular(4.0),
), ),
child: FlowyButton( child: FlowyButton(
leftIcon: item.icon,
margin: const EdgeInsets.symmetric(horizontal: 6), margin: const EdgeInsets.symmetric(horizontal: 6),
iconPadding: 10.0, iconPadding: 10.0,
text: FlowyText.regular( text: FlowyText.regular(
@ -166,11 +170,13 @@ class ActionList extends StatefulWidget {
required this.handler, required this.handler,
required this.onDismiss, required this.onDismiss,
required this.pages, required this.pages,
required this.isLoading,
}); });
final ChatActionHandler handler; final ChatActionHandler handler;
final VoidCallback? onDismiss; final VoidCallback? onDismiss;
final List<ChatInputActionPage> pages; final List<ChatInputActionPage> pages;
final bool isLoading;
@override @override
State<ActionList> createState() => _ActionListState(); State<ActionList> createState() => _ActionListState();
@ -223,13 +229,22 @@ class _ActionListState extends State<ActionList> {
child: ListView( child: ListView(
shrinkWrap: true, shrinkWrap: true,
controller: _scrollController, controller: _scrollController,
padding: const EdgeInsets.all(8), padding: const EdgeInsets.all(4),
children: _buildPages(), children: _buildPages(),
), ),
); );
} }
List<Widget> _buildPages() { List<Widget> _buildPages() {
if (widget.isLoading) {
return [
SizedBox(
height: _noPageHeight.toDouble(),
child: const Center(child: CircularProgressIndicator.adaptive()),
),
];
}
if (widget.pages.isEmpty) { if (widget.pages.isEmpty) {
return [ return [
SizedBox( SizedBox(

View File

@ -30,13 +30,13 @@ impl Stream for LocalAIStreamAdaptor {
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let this = self.project(); let this = self.project();
return match ready!(this.stream.as_mut().poll_next(cx)) { match ready!(this.stream.as_mut().poll_next(cx)) {
Some(Ok(bytes)) => match String::from_utf8(bytes.to_vec()) { Some(Ok(bytes)) => match String::from_utf8(bytes.to_vec()) {
Ok(s) => Poll::Ready(Some(Ok(QuestionStreamValue::Answer { value: s }))), Ok(s) => Poll::Ready(Some(Ok(QuestionStreamValue::Answer { value: s }))),
Err(err) => Poll::Ready(Some(Err(FlowyError::internal().with_context(err)))), Err(err) => Poll::Ready(Some(Err(FlowyError::internal().with_context(err)))),
}, },
Some(Err(err)) => Poll::Ready(Some(Err(FlowyError::local_ai().with_context(err)))), Some(Err(err)) => Poll::Ready(Some(Err(FlowyError::local_ai().with_context(err)))),
None => Poll::Ready(None), None => Poll::Ready(None),
}; }
} }
} }

View File

@ -10,7 +10,7 @@ use crate::manager_observer::{
ChildViewChangeReason, ChildViewChangeReason,
}; };
use crate::notification::{ use crate::notification::{
send_notification, send_workspace_setting_notification, FolderNotification, send_current_workspace_notification, send_notification, FolderNotification,
}; };
use crate::publish_util::{generate_publish_name, view_pb_to_publish_view}; use crate::publish_util::{generate_publish_name, view_pb_to_publish_view};
use crate::share::{ImportParams, ImportValue}; use crate::share::{ImportParams, ImportValue};
@ -978,7 +978,11 @@ impl FolderManager {
} }
let workspace_id = self.user.workspace_id()?; let workspace_id = self.user.workspace_id()?;
send_workspace_setting_notification(workspace_id, view); let setting = WorkspaceSettingPB {
workspace_id,
latest_view: view,
};
send_current_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting);
Ok(()) Ok(())
} }

View File

@ -2,8 +2,6 @@ use flowy_derive::ProtoBuf_Enum;
use flowy_notification::NotificationBuilder; use flowy_notification::NotificationBuilder;
use lib_dispatch::prelude::ToBytes; use lib_dispatch::prelude::ToBytes;
use crate::entities::{ViewPB, WorkspaceSettingPB};
const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace"; const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace";
#[derive(ProtoBuf_Enum, Debug, Default)] #[derive(ProtoBuf_Enum, Debug, Default)]
@ -78,20 +76,8 @@ pub(crate) fn send_notification(id: &str, ty: FolderNotification) -> Notificatio
/// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the /// The [CURRENT_WORKSPACE] represents as the current workspace that opened by the
/// user. Only one workspace can be opened at a time. /// user. Only one workspace can be opened at a time.
const CURRENT_WORKSPACE: &str = "current-workspace"; const CURRENT_WORKSPACE: &str = "current-workspace";
pub(crate) fn send_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) { pub(crate) fn send_current_workspace_notification<T: ToBytes>(ty: FolderNotification, payload: T) {
send_notification(CURRENT_WORKSPACE, ty) send_notification(CURRENT_WORKSPACE, ty)
.payload(payload) .payload(payload)
.send(); .send();
} }
pub(crate) fn send_workspace_setting_notification(
workspace_id: String,
latest_view: Option<ViewPB>,
) -> Option<()> {
let setting = WorkspaceSettingPB {
workspace_id,
latest_view,
};
send_workspace_notification(FolderNotification::DidUpdateWorkspaceSetting, setting);
None
}