mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
chore: polish ui (#5883)
* chore: polish ui * chore: clippy * chore: fmt
This commit is contained in:
parent
453e6309d5
commit
5757cc9a1d
@ -175,7 +175,7 @@ SPEC CHECKSUMS:
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
fluttertoast: 723e187574b149e68e63ca4d39b837586b903cfa
|
||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
@ -197,4 +197,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.11.3
|
||||
|
@ -2,10 +2,12 @@ import 'dart:async';
|
||||
|
||||
import 'package:appflowy/workspace/application/view/view_ext.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:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'chat_input_action_control.dart';
|
||||
@ -43,34 +45,28 @@ class ChatInputActionBloc
|
||||
);
|
||||
},
|
||||
refreshViews: (List<ViewPB> views) {
|
||||
final List<ViewActionPage> pages = _filterPages(
|
||||
views,
|
||||
state.selectedPages,
|
||||
state.filter,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
views: views,
|
||||
pages: views.map((v) => ViewActionPage(view: v)).toList(),
|
||||
pages: pages,
|
||||
indicator: const ChatActionMenuIndicator.ready(),
|
||||
),
|
||||
);
|
||||
},
|
||||
filter: (String filter) {
|
||||
final List<ViewActionPage> pages = [];
|
||||
if (filter.isEmpty) {
|
||||
pages.addAll(state.views.map((v) => ViewActionPage(view: v)));
|
||||
} else {
|
||||
pages.addAll(
|
||||
state.views
|
||||
.where(
|
||||
(v) => v.name.toLowerCase().contains(
|
||||
filter.toLowerCase(),
|
||||
),
|
||||
)
|
||||
.map(
|
||||
(v) => ViewActionPage(view: v),
|
||||
),
|
||||
);
|
||||
}
|
||||
pages.retainWhere((view) {
|
||||
return !state.selectedPages.contains(view);
|
||||
});
|
||||
emit(state.copyWith(pages: pages));
|
||||
Log.debug("Filter chat input pages: $filter");
|
||||
final List<ViewActionPage> pages = _filterPages(
|
||||
state.views,
|
||||
state.selectedPages,
|
||||
filter,
|
||||
);
|
||||
|
||||
emit(state.copyWith(pages: pages, filter: filter));
|
||||
},
|
||||
handleKeyEvent: (PhysicalKeyboardKey physicalKey) {
|
||||
emit(
|
||||
@ -81,8 +77,14 @@ class ChatInputActionBloc
|
||||
},
|
||||
addPage: (ChatInputActionPage page) {
|
||||
if (!state.selectedPages.any((p) => p.pageId == page.pageId)) {
|
||||
final List<ViewActionPage> pages = _filterPages(
|
||||
state.views,
|
||||
state.selectedPages,
|
||||
state.filter,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
pages: pages,
|
||||
selectedPages: [...state.selectedPages, page],
|
||||
),
|
||||
);
|
||||
@ -93,9 +95,11 @@ class ChatInputActionBloc
|
||||
List.from(state.selectedPages);
|
||||
selectedPages.retainWhere((t) => !text.contains(t.title));
|
||||
|
||||
final allPages =
|
||||
state.views.map((v) => ViewActionPage(view: v)).toList();
|
||||
allPages.retainWhere((view) => !selectedPages.contains(view));
|
||||
final List<ViewActionPage> allPages = _filterPages(
|
||||
state.views,
|
||||
state.selectedPages,
|
||||
state.filter,
|
||||
);
|
||||
|
||||
emit(
|
||||
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 {
|
||||
ViewActionPage({required this.view});
|
||||
|
||||
@ -124,6 +154,9 @@ class ViewActionPage extends ChatInputActionPage {
|
||||
|
||||
@override
|
||||
dynamic get page => view;
|
||||
|
||||
@override
|
||||
Widget get icon => view.defaultIcon();
|
||||
}
|
||||
|
||||
@freezed
|
||||
@ -146,7 +179,10 @@ class ChatInputActionState with _$ChatInputActionState {
|
||||
@Default([]) List<ViewPB> views,
|
||||
@Default([]) List<ChatInputActionPage> pages,
|
||||
@Default([]) List<ChatInputActionPage> selectedPages,
|
||||
@Default("") String filter,
|
||||
ChatInputKeyboardEvent? keyboardKey,
|
||||
@Default(ChatActionMenuIndicator.loading())
|
||||
ChatActionMenuIndicator indicator,
|
||||
}) = _ChatInputActionState;
|
||||
}
|
||||
|
||||
@ -159,3 +195,9 @@ class ChatInputKeyboardEvent extends Equatable {
|
||||
@override
|
||||
List<Object?> get props => [timestamp];
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatActionMenuIndicator with _$ChatActionMenuIndicator {
|
||||
const factory ChatActionMenuIndicator.ready() = _Ready;
|
||||
const factory ChatActionMenuIndicator.loading() = _Loading;
|
||||
}
|
||||
|
@ -9,6 +9,7 @@ abstract class ChatInputActionPage extends Equatable {
|
||||
String get title;
|
||||
String get pageId;
|
||||
dynamic get page;
|
||||
Widget get icon;
|
||||
}
|
||||
|
||||
typedef ChatInputMetadata = Map<String, ChatInputActionPage>;
|
||||
@ -26,10 +27,9 @@ class ChatInputActionControl extends ChatActionHandler {
|
||||
final String chatId;
|
||||
|
||||
// Private attributes
|
||||
bool _isShowActionMenu = false;
|
||||
String _atText = "";
|
||||
String _prevText = "";
|
||||
bool _didLoadViews = false;
|
||||
String _showMenuText = "";
|
||||
|
||||
// Getter
|
||||
List<String> get tags =>
|
||||
@ -43,12 +43,12 @@ class ChatInputActionControl extends ChatActionHandler {
|
||||
void handleKeyEvent(KeyEvent event) {
|
||||
// ignore: deprecated_member_use
|
||||
if (event is KeyDownEvent || event is RawKeyDownEvent) {
|
||||
commandBloc.add(ChatInputActionEvent.handleKeyEvent(event.physicalKey));
|
||||
_commandBloc.add(ChatInputActionEvent.handleKeyEvent(event.physicalKey));
|
||||
}
|
||||
}
|
||||
|
||||
bool canHandleKeyEvent(KeyEvent event) {
|
||||
return _isShowActionMenu &&
|
||||
return _showMenuText.isNotEmpty &&
|
||||
<PhysicalKeyboardKey>{
|
||||
PhysicalKeyboardKey.arrowDown,
|
||||
PhysicalKeyboardKey.arrowUp,
|
||||
@ -58,35 +58,29 @@ class ChatInputActionControl extends ChatActionHandler {
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
commandBloc.close();
|
||||
_commandBloc.close();
|
||||
}
|
||||
|
||||
@override
|
||||
void onSelected(ChatInputActionPage page) {
|
||||
_atText = "";
|
||||
_isShowActionMenu = false;
|
||||
|
||||
_commandBloc.add(ChatInputActionEvent.addPage(page));
|
||||
textController.text =
|
||||
"${textController.text.replaceAll(_atText, '')}${page.title}";
|
||||
_prevText = textController.text;
|
||||
textController.text = "$_showMenuText${page.title}";
|
||||
|
||||
onExit();
|
||||
}
|
||||
|
||||
@override
|
||||
void onExit() {
|
||||
_atText = "";
|
||||
_isShowActionMenu = false;
|
||||
_didLoadViews = false;
|
||||
commandBloc.add(const ChatInputActionEvent.filter(""));
|
||||
_showMenuText = "";
|
||||
_prevText = "";
|
||||
_commandBloc.add(const ChatInputActionEvent.filter(""));
|
||||
}
|
||||
|
||||
@override
|
||||
void onEnter() {
|
||||
if (!_didLoadViews) {
|
||||
_didLoadViews = true;
|
||||
commandBloc.add(const ChatInputActionEvent.started());
|
||||
}
|
||||
_isShowActionMenu = true;
|
||||
_commandBloc.add(const ChatInputActionEvent.started());
|
||||
_showMenuText = textController.text;
|
||||
}
|
||||
|
||||
@override
|
||||
@ -134,16 +128,15 @@ class ChatInputActionControl extends ChatActionHandler {
|
||||
}
|
||||
|
||||
// If the action menu is shown, filter the views
|
||||
if (_isShowActionMenu) {
|
||||
// before filter the views, remove the first character '@' if it exists
|
||||
if (inputText.startsWith("@")) {
|
||||
final filter = inputText.substring(1);
|
||||
commandBloc.add(ChatInputActionEvent.filter(filter));
|
||||
if (_showMenuText.isNotEmpty) {
|
||||
if (text.length >= _showMenuText.length) {
|
||||
final filterText = inputText.substring(_showMenuText.length);
|
||||
_commandBloc.add(ChatInputActionEvent.filter(filterText));
|
||||
}
|
||||
|
||||
// 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)) {
|
||||
commandBloc.add(
|
||||
_commandBloc.add(
|
||||
const ChatInputActionEvent.handleKeyEvent(PhysicalKeyboardKey.escape),
|
||||
);
|
||||
}
|
||||
|
@ -489,6 +489,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
child: BlocBuilder<ChatInputStateBloc, ChatInputStateState>(
|
||||
builder: (context, state) {
|
||||
// Show different hint text based on the AI type
|
||||
final aiType = state.aiType;
|
||||
final hintText = state.aiType.when(
|
||||
appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
|
||||
localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
|
||||
@ -500,6 +501,7 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
selector: (state) => state.streamingStatus,
|
||||
builder: (context, state) {
|
||||
return ChatInput(
|
||||
aiType: aiType,
|
||||
chatId: widget.view.id,
|
||||
onSendPressed: (message) {
|
||||
context.read<ChatBloc>().add(
|
||||
|
@ -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/application/chat_input_action_control.dart';
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
@ -26,6 +27,7 @@ class ChatInput extends StatefulWidget {
|
||||
required this.isStreaming,
|
||||
required this.onStopStreaming,
|
||||
required this.hintText,
|
||||
required this.aiType,
|
||||
});
|
||||
|
||||
final bool? isAttachmentUploading;
|
||||
@ -36,6 +38,7 @@ class ChatInput extends StatefulWidget {
|
||||
final String chatId;
|
||||
final bool isStreaming;
|
||||
final String hintText;
|
||||
final AIType aiType;
|
||||
|
||||
@override
|
||||
State<ChatInput> createState() => _ChatInputState();
|
||||
@ -117,7 +120,9 @@ class _ChatInputState extends State<ChatInput> {
|
||||
Expanded(child: _inputTextField(textPadding)),
|
||||
|
||||
// TODO(lucas): support mobile
|
||||
if (PlatformExtension.isDesktop) _atButton(buttonPadding),
|
||||
if (PlatformExtension.isDesktop &&
|
||||
widget.aiType == const AIType.appflowyAI())
|
||||
_atButton(buttonPadding),
|
||||
_sendButton(buttonPadding),
|
||||
const HSpace(14),
|
||||
],
|
||||
@ -169,6 +174,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: widget.hintText,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
|
||||
),
|
||||
@ -190,6 +196,10 @@ class _ChatInputState extends State<ChatInput> {
|
||||
}
|
||||
|
||||
void _handleOnTextChange(BuildContext context, String text) {
|
||||
if (widget.aiType != const AIType.appflowyAI()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (PlatformExtension.isDesktop) {
|
||||
if (_inputActionControl.onTextChanged(text)) {
|
||||
ChatActionsMenu(
|
||||
|
@ -78,6 +78,8 @@ class ChatActionsMenu {
|
||||
),
|
||||
maxHeight,
|
||||
);
|
||||
final isLoading =
|
||||
state.indicator == const ChatActionMenuIndicator.loading();
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
@ -106,6 +108,7 @@ class ChatActionsMenu {
|
||||
vertical: 2,
|
||||
),
|
||||
child: ActionList(
|
||||
isLoading: isLoading,
|
||||
handler: handler,
|
||||
onDismiss: () => dismiss(),
|
||||
pages: state.pages,
|
||||
@ -149,6 +152,7 @@ class _ActionItem extends StatelessWidget {
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: FlowyButton(
|
||||
leftIcon: item.icon,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
iconPadding: 10.0,
|
||||
text: FlowyText.regular(
|
||||
@ -166,11 +170,13 @@ class ActionList extends StatefulWidget {
|
||||
required this.handler,
|
||||
required this.onDismiss,
|
||||
required this.pages,
|
||||
required this.isLoading,
|
||||
});
|
||||
|
||||
final ChatActionHandler handler;
|
||||
final VoidCallback? onDismiss;
|
||||
final List<ChatInputActionPage> pages;
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
State<ActionList> createState() => _ActionListState();
|
||||
@ -223,13 +229,22 @@ class _ActionListState extends State<ActionList> {
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(8),
|
||||
padding: const EdgeInsets.all(4),
|
||||
children: _buildPages(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildPages() {
|
||||
if (widget.isLoading) {
|
||||
return [
|
||||
SizedBox(
|
||||
height: _noPageHeight.toDouble(),
|
||||
child: const Center(child: CircularProgressIndicator.adaptive()),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
if (widget.pages.isEmpty) {
|
||||
return [
|
||||
SizedBox(
|
||||
|
@ -30,13 +30,13 @@ impl Stream for LocalAIStreamAdaptor {
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
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()) {
|
||||
Ok(s) => Poll::Ready(Some(Ok(QuestionStreamValue::Answer { value: s }))),
|
||||
Err(err) => Poll::Ready(Some(Err(FlowyError::internal().with_context(err)))),
|
||||
},
|
||||
Some(Err(err)) => Poll::Ready(Some(Err(FlowyError::local_ai().with_context(err)))),
|
||||
None => Poll::Ready(None),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ use crate::manager_observer::{
|
||||
ChildViewChangeReason,
|
||||
};
|
||||
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::share::{ImportParams, ImportValue};
|
||||
@ -978,7 +978,11 @@ impl FolderManager {
|
||||
}
|
||||
|
||||
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(())
|
||||
}
|
||||
|
||||
|
@ -2,8 +2,6 @@ use flowy_derive::ProtoBuf_Enum;
|
||||
use flowy_notification::NotificationBuilder;
|
||||
use lib_dispatch::prelude::ToBytes;
|
||||
|
||||
use crate::entities::{ViewPB, WorkspaceSettingPB};
|
||||
|
||||
const FOLDER_OBSERVABLE_SOURCE: &str = "Workspace";
|
||||
|
||||
#[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
|
||||
/// user. Only one workspace can be opened at a time.
|
||||
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)
|
||||
.payload(payload)
|
||||
.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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user