mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge branch 'AppFlowy-IO:main' into feat/kanban-clickable-links
This commit is contained in:
commit
ed8d4c17fe
11
CHANGELOG.md
11
CHANGELOG.md
@ -1,4 +1,15 @@
|
||||
# Release Notes
|
||||
## Version 0.6.7 - 13/08/2024
|
||||
### New Features
|
||||
- Redesigned the icon picker design on Desktop.
|
||||
- Redesigned the notification page on Mobile.
|
||||
|
||||
### Bug Fixes
|
||||
- Enhance the toolbar tooltip functionality on Desktop.
|
||||
- Enhance the slash menu user experience on Desktop.
|
||||
- Fixed the issue where list style overrides occurred during text pasting.
|
||||
- Fixed the issue where linking multiple databases in the same document could cause random loss of focus.
|
||||
|
||||
## Version 0.6.6 - 30/07/2024
|
||||
### New Features
|
||||
- Upgrade your workspace to a premium plan to unlock more features and storage.
|
||||
|
0
frontend/appflowy_flutter/build.yaml
Normal file
0
frontend/appflowy_flutter/build.yaml
Normal file
@ -165,6 +165,44 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste text on part of bullet list', (tester) async {
|
||||
const plainText = 'test';
|
||||
|
||||
await tester.pasteContent(
|
||||
plainText: plainText,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "bullet list"},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: [0], offset: 7),
|
||||
end: Position(path: [0], offset: 11),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||
expect(node?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
|
@ -48,8 +48,6 @@ PODS:
|
||||
- fluttertoast (0.0.2):
|
||||
- Flutter
|
||||
- Toast
|
||||
- image_gallery_saver (2.0.2):
|
||||
- Flutter
|
||||
- image_picker_ios (0.0.1):
|
||||
- Flutter
|
||||
- integration_test (0.0.1):
|
||||
@ -95,7 +93,6 @@ DEPENDENCIES:
|
||||
- flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`)
|
||||
- Flutter (from `Flutter`)
|
||||
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
|
||||
- image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
|
||||
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
|
||||
- integration_test (from `.symlinks/plugins/integration_test/ios`)
|
||||
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
|
||||
@ -136,8 +133,6 @@ EXTERNAL SOURCES:
|
||||
:path: Flutter
|
||||
fluttertoast:
|
||||
:path: ".symlinks/plugins/fluttertoast/ios"
|
||||
image_gallery_saver:
|
||||
:path: ".symlinks/plugins/image_gallery_saver/ios"
|
||||
image_picker_ios:
|
||||
:path: ".symlinks/plugins/image_picker_ios/ios"
|
||||
integration_test:
|
||||
@ -176,7 +171,6 @@ SPEC CHECKSUMS:
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
|
@ -102,6 +102,7 @@ class _TypeOptionMenuItem<T> extends StatelessWidget {
|
||||
value.text,
|
||||
fontSize: 14.0,
|
||||
maxLines: 2,
|
||||
lineHeight: 1.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
|
@ -87,6 +87,7 @@ class _PropertyCellState extends State<_PropertyCell> {
|
||||
fieldInfo.name,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 14,
|
||||
figmaLineHeight: 16.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
|
@ -133,7 +133,7 @@ enum DatabaseViewSettings {
|
||||
filter => FlowySvgs.filter_s,
|
||||
sort => FlowySvgs.sort_ascending_s,
|
||||
board => FlowySvgs.board_s,
|
||||
calendar => FlowySvgs.date_s,
|
||||
calendar => FlowySvgs.calendar_s,
|
||||
duplicate => FlowySvgs.copy_s,
|
||||
delete => FlowySvgs.delete_s,
|
||||
};
|
||||
@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
databaseLayoutFromViewLayout(view.layout).layoutName,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
|
@ -4,6 +4,7 @@ import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
|
||||
import 'package:appflowy/shared/red_dot.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
@ -162,7 +163,7 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
|
||||
const Positioned(
|
||||
top: 2,
|
||||
right: 4,
|
||||
child: _RedDot(),
|
||||
child: NotificationRedDot(),
|
||||
),
|
||||
],
|
||||
);
|
||||
@ -172,25 +173,6 @@ class _NotificationNavigationBarItemIcon extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _RedDot extends StatelessWidget {
|
||||
const _RedDot();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: const Color(0xFFFF2214),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _HomePageNavigationBar extends StatelessWidget {
|
||||
const _HomePageNavigationBar({
|
||||
required this.navigationShell,
|
||||
@ -230,11 +212,13 @@ class _HomePageNavigationBar extends StatelessWidget {
|
||||
/// Navigate to the current location of the branch at the provided index when
|
||||
/// tapping an item in the BottomNavigationBar.
|
||||
void _onTap(BuildContext context, int bottomBarIndex) {
|
||||
if (_items[bottomBarIndex].label == _addLabel) {
|
||||
final label = _items[bottomBarIndex].label;
|
||||
if (label == _addLabel) {
|
||||
// show an add dialog
|
||||
mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;
|
||||
|
||||
return;
|
||||
} else if (label == _notificationLabel) {
|
||||
getIt<ReminderBloc>().add(const ReminderEvent.refresh());
|
||||
}
|
||||
// When navigating to a new branch, it's recommended to use the goBranch
|
||||
// method, as doing so makes sure the last navigation state of the
|
||||
|
@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
name,
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
|
@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
languageFromLocale(locale),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
|
@ -16,13 +16,13 @@ part 'chat_ai_message_bloc.freezed.dart';
|
||||
class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
ChatAIMessageBloc({
|
||||
dynamic message,
|
||||
String? metadata,
|
||||
String? refSourceJsonString,
|
||||
required this.chatId,
|
||||
required this.questionId,
|
||||
}) : super(
|
||||
ChatAIMessageState.initial(
|
||||
message,
|
||||
messageRefSourceFromString(metadata),
|
||||
messageReferenceSource(refSourceJsonString),
|
||||
),
|
||||
) {
|
||||
if (state.stream != null) {
|
||||
|
@ -39,7 +39,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
final String chatId;
|
||||
|
||||
/// The last streaming message id
|
||||
String lastStreamMessageId = '';
|
||||
String answerStreamMessageId = '';
|
||||
String questionStreamMessageId = '';
|
||||
|
||||
/// Using a temporary map to associate the real message ID with the last streaming message ID.
|
||||
///
|
||||
@ -87,7 +88,13 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
Int64? beforeMessageId;
|
||||
final oldestMessage = _getOlderstMessage();
|
||||
if (oldestMessage != null) {
|
||||
beforeMessageId = Int64.parseInt(oldestMessage.id);
|
||||
try {
|
||||
beforeMessageId = Int64.parseInt(oldestMessage.id);
|
||||
} catch (e) {
|
||||
Log.error(
|
||||
"Failed to parse message id: $e, messaeg_id: ${oldestMessage.id}",
|
||||
);
|
||||
}
|
||||
}
|
||||
_loadPrevMessage(beforeMessageId);
|
||||
emit(
|
||||
@ -127,21 +134,11 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
},
|
||||
// streaming message
|
||||
streaming: (Message message) {
|
||||
final allMessages = _perminentMessages();
|
||||
allMessages.insert(0, message);
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: allMessages,
|
||||
streamingState: const StreamingState.streaming(),
|
||||
canSendMessage: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
finishStreaming: () {
|
||||
finishAnswerStreaming: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
streamingState: const StreamingState.done(),
|
||||
acceptRelatedQuestion: true,
|
||||
canSendMessage:
|
||||
state.sendingState == const SendMessageState.done(),
|
||||
),
|
||||
@ -162,9 +159,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
// If the streaming is not started, remove the message from the list
|
||||
if (!state.answerStream!.hasStarted) {
|
||||
allMessages.removeWhere(
|
||||
(element) => element.id == lastStreamMessageId,
|
||||
(element) => element.id == answerStreamMessageId,
|
||||
);
|
||||
lastStreamMessageId = "";
|
||||
answerStreamMessageId = "";
|
||||
}
|
||||
|
||||
// when stop stream, we will set the answer stream to null. Which means the streaming
|
||||
@ -189,22 +186,26 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
startAnswerStreaming: (Message message) {
|
||||
final allMessages = _perminentMessages();
|
||||
allMessages.insert(0, message);
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: allMessages,
|
||||
streamingState: const StreamingState.streaming(),
|
||||
canSendMessage: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
sendMessage: (String message, Map<String, dynamic>? metadata) async {
|
||||
unawaited(_startStreamingMessage(message, metadata, emit));
|
||||
final allMessages = _perminentMessages();
|
||||
// allMessages.insert(
|
||||
// 0,
|
||||
// CustomMessage(
|
||||
// metadata: OnetimeShotType.sendingMessage.toMap(),
|
||||
// author: User(id: state.userProfile.id.toString()),
|
||||
// id: state.userProfile.id.toString(),
|
||||
// ),
|
||||
// );
|
||||
emit(
|
||||
state.copyWith(
|
||||
lastSentMessage: null,
|
||||
messages: allMessages,
|
||||
relatedQuestions: [],
|
||||
acceptRelatedQuestion: false,
|
||||
sendingState: const SendMessageState.sending(),
|
||||
canSendMessage: false,
|
||||
),
|
||||
@ -257,10 +258,17 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
chatMessageCallback: (pb) {
|
||||
if (!isClosed) {
|
||||
// 3 mean message response from AI
|
||||
if (pb.authorType == 3 && lastStreamMessageId.isNotEmpty) {
|
||||
if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) {
|
||||
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||
lastStreamMessageId;
|
||||
lastStreamMessageId = "";
|
||||
answerStreamMessageId;
|
||||
answerStreamMessageId = "";
|
||||
}
|
||||
|
||||
// 1 mean message response from User
|
||||
if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) {
|
||||
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||
questionStreamMessageId;
|
||||
questionStreamMessageId = "";
|
||||
}
|
||||
|
||||
final message = _createTextMessage(pb);
|
||||
@ -270,7 +278,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
chatErrorMessageCallback: (err) {
|
||||
if (!isClosed) {
|
||||
Log.error("chat error: ${err.errorMessage}");
|
||||
add(const ChatEvent.finishStreaming());
|
||||
add(const ChatEvent.finishAnswerStreaming());
|
||||
}
|
||||
},
|
||||
latestMessageCallback: (list) {
|
||||
@ -287,7 +295,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
},
|
||||
finishStreamingCallback: () {
|
||||
if (!isClosed) {
|
||||
add(const ChatEvent.finishStreaming());
|
||||
add(const ChatEvent.finishAnswerStreaming());
|
||||
// The answer strema will bet set to null after the streaming is finished or canceled.
|
||||
// so if the answer stream is null, we will not get related question.
|
||||
if (state.lastSentMessage != null && state.answerStream != null) {
|
||||
@ -300,7 +308,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
if (!isClosed) {
|
||||
result.fold(
|
||||
(list) {
|
||||
add(ChatEvent.didReceiveRelatedQuestion(list.items));
|
||||
if (state.acceptRelatedQuestion) {
|
||||
add(ChatEvent.didReceiveRelatedQuestion(list.items));
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
Log.error("Failed to get related question: $err");
|
||||
@ -358,16 +368,24 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
}
|
||||
|
||||
final answerStream = AnswerStream();
|
||||
final questionStream = QuestionStream();
|
||||
add(ChatEvent.didUpdateAnswerStream(answerStream));
|
||||
|
||||
final payload = StreamChatPayloadPB(
|
||||
chatId: state.view.id,
|
||||
message: message,
|
||||
messageType: ChatMessageTypePB.User,
|
||||
textStreamPort: Int64(answerStream.nativePort),
|
||||
questionStreamPort: Int64(questionStream.nativePort),
|
||||
answerStreamPort: Int64(answerStream.nativePort),
|
||||
metadata: await metadataPBFromMetadata(metadata),
|
||||
);
|
||||
|
||||
final questionStreamMessage = _createQuestionStreamMessage(
|
||||
questionStream,
|
||||
metadata,
|
||||
);
|
||||
add(ChatEvent.receveMessage(questionStreamMessage));
|
||||
|
||||
// Stream message to the server
|
||||
final result = await AIEventStreamMessage(payload).send();
|
||||
result.fold(
|
||||
@ -375,13 +393,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
if (!isClosed) {
|
||||
add(ChatEvent.finishSending(question));
|
||||
|
||||
final questionMessageId = question.messageId;
|
||||
final message = _createTextMessage(question);
|
||||
add(ChatEvent.receveMessage(message));
|
||||
// final message = _createTextMessage(question);
|
||||
// add(ChatEvent.receveMessage(message));
|
||||
|
||||
final streamAnswer =
|
||||
_createStreamMessage(answerStream, questionMessageId);
|
||||
add(ChatEvent.streaming(streamAnswer));
|
||||
_createAnswerStreamMessage(answerStream, question.messageId);
|
||||
add(ChatEvent.startAnswerStreaming(streamAnswer));
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
@ -404,9 +421,12 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
}
|
||||
|
||||
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
|
||||
Message _createAnswerStreamMessage(
|
||||
AnswerStream stream,
|
||||
Int64 questionMessageId,
|
||||
) {
|
||||
final streamMessageId = (questionMessageId + 1).toString();
|
||||
lastStreamMessageId = streamMessageId;
|
||||
answerStreamMessageId = streamMessageId;
|
||||
|
||||
return TextMessage(
|
||||
author: User(id: "streamId:${nanoid()}"),
|
||||
@ -421,6 +441,32 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
}
|
||||
|
||||
Message _createQuestionStreamMessage(
|
||||
QuestionStream stream,
|
||||
Map<String, dynamic>? sentMetadata,
|
||||
) {
|
||||
final now = DateTime.now();
|
||||
final timestamp = now.millisecondsSinceEpoch;
|
||||
questionStreamMessageId = timestamp.toString();
|
||||
final Map<String, dynamic> metadata = {};
|
||||
|
||||
// if (sentMetadata != null) {
|
||||
// metadata[messageMetadataJsonStringKey] = sentMetadata;
|
||||
// }
|
||||
|
||||
metadata["$QuestionStream"] = stream;
|
||||
metadata["chatId"] = chatId;
|
||||
metadata[messageChatFileListKey] =
|
||||
chatFilesFromMessageMetadata(sentMetadata);
|
||||
return TextMessage(
|
||||
author: User(id: state.userProfile.id.toString()),
|
||||
metadata: metadata,
|
||||
id: questionStreamMessageId,
|
||||
createdAt: DateTime.now().millisecondsSinceEpoch,
|
||||
text: '',
|
||||
);
|
||||
}
|
||||
|
||||
Message _createTextMessage(ChatMessagePB message) {
|
||||
String messageId = message.messageId.toString();
|
||||
|
||||
@ -435,7 +481,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
text: message.content,
|
||||
createdAt: message.createdAt.toInt() * 1000,
|
||||
metadata: {
|
||||
messageMetadataKey: message.metadata,
|
||||
messageRefSourceJsonStringKey: message.metadata,
|
||||
},
|
||||
);
|
||||
}
|
||||
@ -454,9 +500,10 @@ class ChatEvent with _$ChatEvent {
|
||||
_FinishSendMessage;
|
||||
|
||||
// receive message
|
||||
const factory ChatEvent.streaming(Message message) = _StreamingMessage;
|
||||
const factory ChatEvent.startAnswerStreaming(Message message) =
|
||||
_StartAnswerStreaming;
|
||||
const factory ChatEvent.receveMessage(Message message) = _ReceiveMessage;
|
||||
const factory ChatEvent.finishStreaming() = _FinishStreamingMessage;
|
||||
const factory ChatEvent.finishAnswerStreaming() = _FinishAnswerStreaming;
|
||||
|
||||
// loading messages
|
||||
const factory ChatEvent.startLoadingPrevMessage() = _StartLoadPrevMessage;
|
||||
@ -499,6 +546,7 @@ class ChatState with _$ChatState {
|
||||
required bool hasMorePrevMessage,
|
||||
// The related questions that are received after the user message is sent.
|
||||
required List<RelatedQuestionPB> relatedQuestions,
|
||||
@Default(false) bool acceptRelatedQuestion,
|
||||
// The last user message that is sent to the server.
|
||||
ChatMessagePB? lastSentMessage,
|
||||
AnswerStream? answerStream,
|
||||
|
@ -15,8 +15,14 @@ part 'chat_entity.freezed.dart';
|
||||
const sendMessageErrorKey = "sendMessageError";
|
||||
const systemUserId = "system";
|
||||
const aiResponseUserId = "0";
|
||||
const messageMetadataKey = "metadata";
|
||||
const messageQuestionIdKey = "question";
|
||||
|
||||
/// `messageRefSourceJsonStringKey` is the key used for metadata that contains the reference source of a message.
|
||||
/// Each message may include this information.
|
||||
/// - When used in a sent message, it indicates that the message includes an attachment.
|
||||
/// - When used in a received message, it indicates the AI reference sources used to answer a question.
|
||||
const messageRefSourceJsonStringKey = "ref_source_json_string";
|
||||
const messageChatFileListKey = "chat_files";
|
||||
const messageQuestionIdKey = "question_id";
|
||||
|
||||
@JsonSerializable()
|
||||
class ChatMessageRefSource {
|
||||
|
@ -1,8 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
@ -10,40 +7,14 @@ part 'chat_input_file_bloc.freezed.dart';
|
||||
|
||||
class ChatInputFileBloc extends Bloc<ChatInputFileEvent, ChatInputFileState> {
|
||||
ChatInputFileBloc({
|
||||
// ignore: avoid_unused_constructor_parameters
|
||||
required String chatId,
|
||||
required this.file,
|
||||
}) : super(const ChatInputFileState()) {
|
||||
on<ChatInputFileEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final payload = ChatFilePB(
|
||||
filePath: file.filePath,
|
||||
chatId: chatId,
|
||||
);
|
||||
unawaited(
|
||||
AIEventChatWithFile(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold(
|
||||
(_) {
|
||||
add(
|
||||
const ChatInputFileEvent.updateUploadState(
|
||||
UploadFileIndicator.finish(),
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
add(
|
||||
ChatInputFileEvent.updateUploadState(
|
||||
UploadFileIndicator.error(err.toString()),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
initial: () async {},
|
||||
updateUploadState: (UploadFileIndicator indicator) {
|
||||
emit(state.copyWith(uploadFileIndicator: indicator));
|
||||
},
|
||||
|
@ -10,6 +10,9 @@ import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
/// Indicate file source from appflowy document
|
||||
const appflowySoruce = "appflowy";
|
||||
|
||||
List<ChatFile> fileListFromMessageMetadata(
|
||||
Map<String, dynamic>? map,
|
||||
) {
|
||||
@ -32,7 +35,12 @@ List<ChatFile> chatFilesFromMetadataString(String? s) {
|
||||
|
||||
final metadataJson = jsonDecode(s);
|
||||
if (metadataJson is Map<String, dynamic>) {
|
||||
return _parseChatFile(metadataJson);
|
||||
final file = chatFileFromMap(metadataJson);
|
||||
if (file != null) {
|
||||
return [file];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
} else if (metadataJson is List) {
|
||||
return metadataJson
|
||||
.map((e) => e as Map<String, dynamic>)
|
||||
@ -46,11 +54,6 @@ List<ChatFile> chatFilesFromMetadataString(String? s) {
|
||||
}
|
||||
}
|
||||
|
||||
List<ChatFile> _parseChatFile(Map<String, dynamic> map) {
|
||||
final file = chatFileFromMap(map);
|
||||
return file != null ? [file] : [];
|
||||
}
|
||||
|
||||
ChatFile? chatFileFromMap(Map<String, dynamic>? map) {
|
||||
if (map == null) return null;
|
||||
|
||||
@ -63,7 +66,7 @@ ChatFile? chatFileFromMap(Map<String, dynamic>? map) {
|
||||
return ChatFile.fromFilePath(filePath);
|
||||
}
|
||||
|
||||
List<ChatMessageRefSource> messageRefSourceFromString(String? s) {
|
||||
List<ChatMessageRefSource> messageReferenceSource(String? s) {
|
||||
if (s == null || s.isEmpty || s == "null") {
|
||||
return [];
|
||||
}
|
||||
@ -75,6 +78,7 @@ List<ChatMessageRefSource> messageRefSourceFromString(String? s) {
|
||||
Log.warn("metadata is null");
|
||||
return [];
|
||||
}
|
||||
// [{"id":null,"name":"The Five Dysfunctions of a Team.pdf","source":"/Users/weidongfu/Desktop/The Five Dysfunctions of a Team.pdf"}]
|
||||
|
||||
if (metadataJson is Map<String, dynamic>) {
|
||||
if (metadataJson.isNotEmpty) {
|
||||
@ -115,7 +119,7 @@ Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
|
||||
name: view.name,
|
||||
data: pb.text,
|
||||
dataType: ChatMessageMetaTypePB.Txt,
|
||||
source: "appflowy",
|
||||
source: appflowySoruce,
|
||||
),
|
||||
);
|
||||
}, (err) {
|
||||
@ -139,3 +143,18 @@ Future<List<ChatMessageMetaPB>> metadataPBFromMetadata(
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
||||
List<ChatFile> chatFilesFromMessageMetadata(
|
||||
Map<String, dynamic>? map,
|
||||
) {
|
||||
final List<ChatFile> metadata = [];
|
||||
if (map != null) {
|
||||
for (final entry in map.entries) {
|
||||
if (entry.value is ChatFile) {
|
||||
metadata.add(entry.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return metadata;
|
||||
}
|
||||
|
@ -25,7 +25,7 @@ class AnswerStream {
|
||||
} else if (event.startsWith("metadata:")) {
|
||||
if (_onMetadata != null) {
|
||||
final s = event.substring(9);
|
||||
_onMetadata!(messageRefSourceFromString(s));
|
||||
_onMetadata!(messageReferenceSource(s));
|
||||
}
|
||||
} else if (event == "AI_RESPONSE_LIMIT") {
|
||||
if (_onAIResponseLimit != null) {
|
||||
@ -92,3 +92,102 @@ class AnswerStream {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class QuestionStream {
|
||||
QuestionStream() {
|
||||
_port.handler = _controller.add;
|
||||
_subscription = _controller.stream.listen(
|
||||
(event) {
|
||||
if (event.startsWith("data:")) {
|
||||
_hasStarted = true;
|
||||
final newText = event.substring(5);
|
||||
_text += newText;
|
||||
if (_onData != null) {
|
||||
_onData!(_text);
|
||||
}
|
||||
} else if (event.startsWith("message_id:")) {
|
||||
final messageId = event.substring(11);
|
||||
_onMessageId?.call(messageId);
|
||||
} else if (event.startsWith("start_index_file:")) {
|
||||
final indexName = event.substring(17);
|
||||
_onFileIndexStart?.call(indexName);
|
||||
} else if (event.startsWith("end_index_file:")) {
|
||||
final indexName = event.substring(10);
|
||||
_onFileIndexEnd?.call(indexName);
|
||||
} else if (event.startsWith("index_file_error:")) {
|
||||
final indexName = event.substring(16);
|
||||
_onFileIndexError?.call(indexName);
|
||||
} else if (event.startsWith("index_start:")) {
|
||||
_onIndexStart?.call();
|
||||
} else if (event.startsWith("index_end:")) {
|
||||
_onIndexEnd?.call();
|
||||
} else if (event.startsWith("done:")) {
|
||||
_onDone?.call();
|
||||
} else if (event.startsWith("error:")) {
|
||||
_error = event.substring(5);
|
||||
if (_onError != null) {
|
||||
_onError!(_error!);
|
||||
}
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (_onError != null) {
|
||||
_onError!(error.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final RawReceivePort _port = RawReceivePort();
|
||||
final StreamController<String> _controller = StreamController.broadcast();
|
||||
late StreamSubscription<String> _subscription;
|
||||
bool _hasStarted = false;
|
||||
String? _error;
|
||||
String _text = "";
|
||||
|
||||
// Callbacks
|
||||
void Function(String text)? _onData;
|
||||
void Function(String error)? _onError;
|
||||
void Function(String messageId)? _onMessageId;
|
||||
void Function(String indexName)? _onFileIndexStart;
|
||||
void Function(String indexName)? _onFileIndexEnd;
|
||||
void Function(String indexName)? _onFileIndexError;
|
||||
void Function()? _onIndexStart;
|
||||
void Function()? _onIndexEnd;
|
||||
void Function()? _onDone;
|
||||
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
bool get hasStarted => _hasStarted;
|
||||
String? get error => _error;
|
||||
String get text => _text;
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _controller.close();
|
||||
await _subscription.cancel();
|
||||
_port.close();
|
||||
}
|
||||
|
||||
void listen({
|
||||
void Function(String text)? onData,
|
||||
void Function(String error)? onError,
|
||||
void Function(String messageId)? onMessageId,
|
||||
void Function(String indexName)? onFileIndexStart,
|
||||
void Function(String indexName)? onFileIndexEnd,
|
||||
void Function(String indexName)? onFileIndexFail,
|
||||
void Function()? onIndexStart,
|
||||
void Function()? onIndexEnd,
|
||||
void Function()? onDone,
|
||||
}) {
|
||||
_onData = onData;
|
||||
_onError = onError;
|
||||
_onMessageId = onMessageId;
|
||||
|
||||
_onFileIndexStart = onFileIndexStart;
|
||||
_onFileIndexEnd = onFileIndexEnd;
|
||||
_onFileIndexError = onFileIndexFail;
|
||||
|
||||
_onIndexStart = onIndexStart;
|
||||
_onIndexEnd = onIndexEnd;
|
||||
_onDone = onDone;
|
||||
}
|
||||
}
|
||||
|
@ -1,27 +1,93 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'chat_message_service.dart';
|
||||
|
||||
part 'chat_user_message_bloc.freezed.dart';
|
||||
|
||||
class ChatUserMessageBloc
|
||||
extends Bloc<ChatUserMessageEvent, ChatUserMessageState> {
|
||||
ChatUserMessageBloc({
|
||||
required Message message,
|
||||
required String? metadata,
|
||||
required dynamic message,
|
||||
}) : super(
|
||||
ChatUserMessageState.initial(
|
||||
message,
|
||||
chatFilesFromMetadataString(metadata),
|
||||
),
|
||||
) {
|
||||
on<ChatUserMessageEvent>(
|
||||
(event, emit) async {
|
||||
(event, emit) {
|
||||
event.when(
|
||||
initial: () {},
|
||||
initial: () {
|
||||
if (state.stream != null) {
|
||||
if (!isClosed) {
|
||||
add(ChatUserMessageEvent.updateText(state.stream!.text));
|
||||
}
|
||||
}
|
||||
|
||||
state.stream?.listen(
|
||||
onData: (text) {
|
||||
if (!isClosed) {
|
||||
add(ChatUserMessageEvent.updateText(text));
|
||||
}
|
||||
},
|
||||
onMessageId: (messageId) {
|
||||
if (!isClosed) {
|
||||
add(ChatUserMessageEvent.updateMessageId(messageId));
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (!isClosed) {
|
||||
add(ChatUserMessageEvent.receiveError(error.toString()));
|
||||
}
|
||||
},
|
||||
onFileIndexStart: (indexName) {
|
||||
Log.debug("index start: $indexName");
|
||||
},
|
||||
onFileIndexEnd: (indexName) {
|
||||
Log.info("index end: $indexName");
|
||||
},
|
||||
onFileIndexFail: (indexName) {
|
||||
Log.debug("index fail: $indexName");
|
||||
},
|
||||
onIndexStart: () {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
const ChatUserMessageEvent.updateQuestionState(
|
||||
QuestionMessageState.indexStart(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onIndexEnd: () {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
const ChatUserMessageEvent.updateQuestionState(
|
||||
QuestionMessageState.indexEnd(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
const ChatUserMessageEvent.updateQuestionState(
|
||||
QuestionMessageState.finish(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
updateText: (String text) {
|
||||
emit(state.copyWith(text: text));
|
||||
},
|
||||
updateMessageId: (String messageId) {
|
||||
emit(state.copyWith(messageId: messageId));
|
||||
},
|
||||
receiveError: (String error) {},
|
||||
updateQuestionState: (QuestionMessageState newState) {
|
||||
emit(state.copyWith(messageState: newState));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@ -31,18 +97,47 @@ class ChatUserMessageBloc
|
||||
@freezed
|
||||
class ChatUserMessageEvent with _$ChatUserMessageEvent {
|
||||
const factory ChatUserMessageEvent.initial() = Initial;
|
||||
const factory ChatUserMessageEvent.updateText(String text) = _UpdateText;
|
||||
const factory ChatUserMessageEvent.updateQuestionState(
|
||||
QuestionMessageState newState,
|
||||
) = _UpdateQuestionState;
|
||||
const factory ChatUserMessageEvent.updateMessageId(String messageId) =
|
||||
_UpdateMessageId;
|
||||
const factory ChatUserMessageEvent.receiveError(String error) = _ReceiveError;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatUserMessageState with _$ChatUserMessageState {
|
||||
const factory ChatUserMessageState({
|
||||
required Message message,
|
||||
required List<ChatFile> files,
|
||||
required String text,
|
||||
QuestionStream? stream,
|
||||
String? messageId,
|
||||
@Default(QuestionMessageState.finish()) QuestionMessageState messageState,
|
||||
}) = _ChatUserMessageState;
|
||||
|
||||
factory ChatUserMessageState.initial(
|
||||
Message message,
|
||||
List<ChatFile> files,
|
||||
dynamic message,
|
||||
) =>
|
||||
ChatUserMessageState(message: message, files: files);
|
||||
ChatUserMessageState(
|
||||
text: message is String ? message : "",
|
||||
stream: message is QuestionStream ? message : null,
|
||||
);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class QuestionMessageState with _$QuestionMessageState {
|
||||
const factory QuestionMessageState.indexFileStart(String fileName) =
|
||||
_IndexFileStart;
|
||||
const factory QuestionMessageState.indexFileEnd(String fileName) =
|
||||
_IndexFileEnd;
|
||||
const factory QuestionMessageState.indexFileFail(String fileName) =
|
||||
_IndexFileFail;
|
||||
|
||||
const factory QuestionMessageState.indexStart() = _IndexStart;
|
||||
const factory QuestionMessageState.indexEnd() = _IndexEnd;
|
||||
const factory QuestionMessageState.finish() = _Finish;
|
||||
}
|
||||
|
||||
extension QuestionMessageStateX on QuestionMessageState {
|
||||
bool get isFinish => this is _Finish;
|
||||
}
|
||||
|
@ -0,0 +1,60 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import 'chat_message_service.dart';
|
||||
|
||||
part 'chat_user_message_bubble_bloc.freezed.dart';
|
||||
|
||||
class ChatUserMessageBubbleBloc
|
||||
extends Bloc<ChatUserMessageBubbleEvent, ChatUserMessageBubbleState> {
|
||||
ChatUserMessageBubbleBloc({
|
||||
required Message message,
|
||||
}) : super(
|
||||
ChatUserMessageBubbleState.initial(
|
||||
message,
|
||||
_getFiles(message.metadata),
|
||||
),
|
||||
) {
|
||||
on<ChatUserMessageBubbleEvent>(
|
||||
(event, emit) async {
|
||||
event.when(
|
||||
initial: () {},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
List<ChatFile> _getFiles(Map<String, dynamic>? metadata) {
|
||||
if (metadata == null) {
|
||||
return [];
|
||||
}
|
||||
final refSourceMetadata = metadata[messageRefSourceJsonStringKey] as String?;
|
||||
final files = metadata[messageChatFileListKey] as List<ChatFile>?;
|
||||
|
||||
if (refSourceMetadata != null) {
|
||||
return chatFilesFromMetadataString(refSourceMetadata);
|
||||
}
|
||||
return files ?? [];
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatUserMessageBubbleEvent with _$ChatUserMessageBubbleEvent {
|
||||
const factory ChatUserMessageBubbleEvent.initial() = Initial;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatUserMessageBubbleState with _$ChatUserMessageBubbleState {
|
||||
const factory ChatUserMessageBubbleState({
|
||||
required Message message,
|
||||
required List<ChatFile> files,
|
||||
}) = _ChatUserMessageBubbleState;
|
||||
|
||||
factory ChatUserMessageBubbleState.initial(
|
||||
Message message,
|
||||
List<ChatFile> files,
|
||||
) =>
|
||||
ChatUserMessageBubbleState(message: message, files: files);
|
||||
}
|
@ -319,25 +319,25 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
|
||||
Widget _buildTextMessage(BuildContext context, TextMessage message) {
|
||||
if (message.author.id == _user.id) {
|
||||
final metadata = message.metadata?[messageMetadataKey] as String?;
|
||||
return ChatUserTextMessageWidget(
|
||||
final stream = message.metadata?["$QuestionStream"];
|
||||
return ChatUserMessageWidget(
|
||||
key: ValueKey(message.id),
|
||||
user: message.author,
|
||||
messageUserId: message.id,
|
||||
message: message,
|
||||
metadata: metadata,
|
||||
message: stream is QuestionStream ? stream : message.text,
|
||||
);
|
||||
} else {
|
||||
final stream = message.metadata?["$AnswerStream"];
|
||||
final questionId = message.metadata?[messageQuestionIdKey];
|
||||
final metadata = message.metadata?[messageMetadataKey] as String?;
|
||||
return ChatAITextMessageWidget(
|
||||
final refSourceJsonString =
|
||||
message.metadata?[messageRefSourceJsonStringKey] as String?;
|
||||
return ChatAIMessageWidget(
|
||||
user: message.author,
|
||||
messageUserId: message.id,
|
||||
text: stream is AnswerStream ? stream : message.text,
|
||||
message: stream is AnswerStream ? stream : message.text,
|
||||
key: ValueKey(message.id),
|
||||
questionId: questionId,
|
||||
chatId: widget.view.id,
|
||||
metadata: metadata,
|
||||
refSourceJsonString: refSourceJsonString,
|
||||
onSelectedMetadata: (ChatMessageRefSource metadata) {
|
||||
context.read<ChatSidePannelBloc>().add(
|
||||
ChatSidePannelEvent.selectedMetadata(metadata),
|
||||
|
@ -12,6 +12,7 @@ import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra/platform_extension.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@ -21,8 +22,8 @@ import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
|
||||
import 'chat_at_button.dart';
|
||||
import 'chat_input_attachment.dart';
|
||||
import 'chat_send_button.dart';
|
||||
import 'chat_input_span.dart';
|
||||
import 'chat_send_button.dart';
|
||||
import 'layout_define.dart';
|
||||
|
||||
class ChatInput extends StatefulWidget {
|
||||
@ -114,7 +115,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: _inputFocusNode.hasFocus && !isMobile
|
||||
color: _inputFocusNode.hasFocus
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.6)
|
||||
: Theme.of(context).colorScheme.secondary,
|
||||
),
|
||||
@ -154,15 +155,16 @@ class _ChatInputState extends State<ChatInput> {
|
||||
children: [
|
||||
// TODO(lucas): support mobile
|
||||
if (PlatformExtension.isDesktop &&
|
||||
widget.aiType == const AIType.localAI())
|
||||
widget.aiType.isLocalAI())
|
||||
_attachmentButton(buttonPadding),
|
||||
|
||||
// text field
|
||||
Expanded(child: _inputTextField(context, textPadding)),
|
||||
|
||||
// at button
|
||||
// TODO(lucas): support mobile
|
||||
if (PlatformExtension.isDesktop) _atButton(buttonPadding),
|
||||
// mention button
|
||||
_mentionButton(buttonPadding),
|
||||
|
||||
if (PlatformExtension.isMobile) const HSpace(6.0),
|
||||
|
||||
// send button
|
||||
_sendButton(buttonPadding),
|
||||
@ -244,6 +246,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
InputDecoration _buildInputDecoration(BuildContext context) {
|
||||
return InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
hintText: widget.hintText,
|
||||
focusedBorder: InputBorder.none,
|
||||
hintStyle: TextStyle(
|
||||
@ -352,7 +355,7 @@ class _ChatInputState extends State<ChatInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _atButton(EdgeInsets buttonPadding) {
|
||||
Widget _mentionButton(EdgeInsets buttonPadding) {
|
||||
return Padding(
|
||||
padding: buttonPadding,
|
||||
child: SizedBox.square(
|
||||
|
@ -17,7 +17,7 @@ class ChatInputAttachment extends StatelessWidget {
|
||||
message: LocaleKeys.chat_uploadFile.tr(),
|
||||
child: FlowyIconButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
radius: BorderRadius.circular(18),
|
||||
radius: BorderRadius.circular(6),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_attachment_s,
|
||||
size: const Size.square(20),
|
||||
|
@ -156,6 +156,7 @@ class _ActionItem extends StatelessWidget {
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
iconPadding: 10.0,
|
||||
text: FlowyText.regular(
|
||||
lineHeight: 1.0,
|
||||
item.title,
|
||||
),
|
||||
onTap: onTap,
|
||||
|
@ -124,6 +124,7 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
|
||||
editorScrollController: scrollController,
|
||||
blockComponentBuilders: blockBuilders,
|
||||
commandShortcutEvents: [customCopyCommand],
|
||||
disableAutoScroll: true,
|
||||
editorState: editorState,
|
||||
),
|
||||
);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_message_service.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@ -49,9 +50,17 @@ class AIMessageMetadata extends StatelessWidget {
|
||||
child: FlowyText(
|
||||
m.name,
|
||||
fontSize: 14,
|
||||
lineHeight: 1.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
onTap: () => onSelectedMetadata(m),
|
||||
disable: m.source != appflowySoruce,
|
||||
onTap: () {
|
||||
if (m.source != appflowySoruce) {
|
||||
return;
|
||||
}
|
||||
onSelectedMetadata(m);
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
@ -14,32 +14,34 @@ import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
|
||||
import 'ai_metadata.dart';
|
||||
|
||||
class ChatAITextMessageWidget extends StatelessWidget {
|
||||
const ChatAITextMessageWidget({
|
||||
class ChatAIMessageWidget extends StatelessWidget {
|
||||
const ChatAIMessageWidget({
|
||||
super.key,
|
||||
required this.user,
|
||||
required this.messageUserId,
|
||||
required this.text,
|
||||
required this.message,
|
||||
required this.questionId,
|
||||
required this.chatId,
|
||||
required this.metadata,
|
||||
required this.refSourceJsonString,
|
||||
required this.onSelectedMetadata,
|
||||
});
|
||||
|
||||
final User user;
|
||||
final String messageUserId;
|
||||
final dynamic text;
|
||||
|
||||
/// message can be a striing or Stream<String>
|
||||
final dynamic message;
|
||||
final Int64? questionId;
|
||||
final String chatId;
|
||||
final String? metadata;
|
||||
final String? refSourceJsonString;
|
||||
final void Function(ChatMessageRefSource metadata) onSelectedMetadata;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => ChatAIMessageBloc(
|
||||
message: text,
|
||||
metadata: metadata,
|
||||
message: message,
|
||||
refSourceJsonString: refSourceJsonString,
|
||||
chatId: chatId,
|
||||
questionId: questionId,
|
||||
)..add(const ChatAIMessageEvent.initial()),
|
||||
@ -58,8 +60,8 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
||||
onAIResponseLimit: () {
|
||||
return FlowyText(
|
||||
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
|
||||
maxLines: 10,
|
||||
lineHeight: 1.5,
|
||||
maxLines: 10,
|
||||
);
|
||||
},
|
||||
ready: () {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_member_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bubble_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
@ -29,14 +29,12 @@ class ChatUserMessageBubble extends StatelessWidget {
|
||||
.read<ChatMemberBloc>()
|
||||
.add(ChatMemberEvent.getMemberInfo(message.author.id));
|
||||
}
|
||||
final metadata = message.metadata?[messageMetadataKey] as String?;
|
||||
|
||||
return BlocProvider(
|
||||
create: (context) => ChatUserMessageBloc(
|
||||
create: (context) => ChatUserMessageBubbleBloc(
|
||||
message: message,
|
||||
metadata: metadata,
|
||||
),
|
||||
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
|
||||
child: BlocBuilder<ChatUserMessageBubbleBloc, ChatUserMessageBubbleState>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
@ -1,26 +1,48 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_user_message_bloc.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
|
||||
class ChatUserTextMessageWidget extends StatelessWidget {
|
||||
const ChatUserTextMessageWidget({
|
||||
class ChatUserMessageWidget extends StatelessWidget {
|
||||
const ChatUserMessageWidget({
|
||||
super.key,
|
||||
required this.user,
|
||||
required this.messageUserId,
|
||||
required this.message,
|
||||
required this.metadata,
|
||||
});
|
||||
|
||||
final User user;
|
||||
final String messageUserId;
|
||||
final TextMessage message;
|
||||
final String? metadata;
|
||||
final dynamic message;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextMessageText(
|
||||
text: message.text,
|
||||
return BlocProvider(
|
||||
create: (context) => ChatUserMessageBloc(message: message)
|
||||
..add(const ChatUserMessageEvent.initial()),
|
||||
child: BlocBuilder<ChatUserMessageBloc, ChatUserMessageState>(
|
||||
builder: (context, state) {
|
||||
final List<Widget> children = [];
|
||||
children.add(
|
||||
Flexible(
|
||||
child: TextMessageText(
|
||||
text: state.text,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (!state.messageState.isFinish) {
|
||||
children.add(const HSpace(6));
|
||||
children.add(const CircularProgressIndicator.adaptive());
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: children,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -237,6 +237,7 @@ class _BoardColumnHeaderState extends State<BoardColumnHeader> {
|
||||
leftIcon: FlowySvg(action.icon),
|
||||
text: FlowyText.medium(
|
||||
action.text,
|
||||
lineHeight: 1.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onTap: () {
|
||||
|
@ -173,12 +173,15 @@ class LayoutDateField extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(fieldInfo.name),
|
||||
text: FlowyText.medium(
|
||||
fieldInfo.name,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onTap: () {
|
||||
onUpdated(fieldInfo.id);
|
||||
popoverMutex.close();
|
||||
},
|
||||
leftIcon: const FlowySvg(FlowySvgs.grid_s),
|
||||
leftIcon: const FlowySvg(FlowySvgs.date_s),
|
||||
rightIcon: fieldInfo.id == fieldId
|
||||
? const FlowySvg(FlowySvgs.check_s)
|
||||
: null,
|
||||
@ -206,6 +209,7 @@ class LayoutDateField extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.calendar_settings_layoutDateField.tr(),
|
||||
),
|
||||
),
|
||||
@ -307,6 +311,7 @@ class FirstDayOfWeek extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 10.0),
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.calendar_settings_firstDayOfWeek.tr(),
|
||||
),
|
||||
),
|
||||
@ -367,7 +372,10 @@ class StartFromButton extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(title),
|
||||
text: FlowyText.medium(
|
||||
title,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onTap: () => onTap(dayIndex),
|
||||
rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,
|
||||
),
|
||||
|
@ -15,6 +15,7 @@ class GridSize {
|
||||
static double get popoverItemHeight => 26 * scale;
|
||||
static double get typeOptionSeparatorHeight => 4 * scale;
|
||||
static double get newPropertyButtonWidth => 140 * scale;
|
||||
static double get mobileNewPropertyButtonWidth => 200 * scale;
|
||||
|
||||
static EdgeInsets get cellContentInsets => EdgeInsets.symmetric(
|
||||
horizontal: GridSize.cellHPadding,
|
||||
|
@ -167,6 +167,7 @@ class _CalculateCellState extends State<CalculateCell> {
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
widget.calculation!.calculationType.shortLabel
|
||||
.toUpperCase(),
|
||||
color: Theme.of(context).hintColor,
|
||||
@ -175,6 +176,7 @@ class _CalculateCellState extends State<CalculateCell> {
|
||||
if (widget.calculation!.value.isNotEmpty) ...[
|
||||
const HSpace(8),
|
||||
FlowyText(
|
||||
lineHeight: 1.0,
|
||||
calculateValue,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
@ -22,7 +22,11 @@ class CalculationTypeItem extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(type.label, overflow: TextOverflow.ellipsis),
|
||||
text: FlowyText.medium(
|
||||
type.label,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onTap: () {
|
||||
onTap();
|
||||
PopoverContainer.of(context).close();
|
||||
|
@ -39,6 +39,7 @@ class ChoiceChipButton extends StatelessWidget {
|
||||
decoration: decoration,
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
lineHeight: 1.0,
|
||||
filterInfo.fieldInfo.field.name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
|
@ -31,6 +31,7 @@ class ConditionButton extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
lineHeight: 1.0,
|
||||
conditionName,
|
||||
fontSize: 10,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
|
@ -163,6 +163,7 @@ class GridFilterPropertyCell extends StatelessWidget {
|
||||
return FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
fieldInfo.field.name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
|
@ -87,6 +87,7 @@ class _AddFilterButtonState extends State<AddFilterButton> {
|
||||
height: 28,
|
||||
child: FlowyButton(
|
||||
text: FlowyText(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.grid_settings_addFilter.tr(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
|
@ -23,6 +23,7 @@ class GridAddRowButton extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
text: FlowyText(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.grid_row_newRow.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
|
@ -229,6 +229,7 @@ class FieldCellButton extends StatelessWidget {
|
||||
radius: radius,
|
||||
text: FlowyText.medium(
|
||||
field.name,
|
||||
lineHeight: 1.0,
|
||||
maxLines: maxLines,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
|
@ -195,6 +195,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
|
||||
margin: GridSize.cellContentInsets,
|
||||
radius: BorderRadius.zero,
|
||||
text: FlowyText(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
@ -197,7 +197,7 @@ class _CreateFieldButtonState extends State<CreateFieldButton> {
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: GridSize.newPropertyButtonWidth,
|
||||
maxWidth: GridSize.mobileNewPropertyButtonWidth,
|
||||
minHeight: GridSize.headerHeight,
|
||||
),
|
||||
decoration: _getDecoration(context),
|
||||
|
@ -51,7 +51,11 @@ class RowActionMenu extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(action.text, overflow: TextOverflow.ellipsis),
|
||||
text: FlowyText.medium(
|
||||
action.text,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onTap: () {
|
||||
if (action == RowAction.delete) {
|
||||
NavigatorOkCancelDialog(
|
||||
@ -82,7 +86,7 @@ enum RowAction {
|
||||
return switch (this) {
|
||||
insertAbove => FlowySvgs.arrow_s,
|
||||
insertBelow => FlowySvgs.add_s,
|
||||
duplicate => FlowySvgs.copy_s,
|
||||
duplicate => FlowySvgs.duplicate_s,
|
||||
delete => FlowySvgs.delete_s,
|
||||
};
|
||||
}
|
||||
|
@ -120,6 +120,7 @@ class GridSortPropertyCell extends StatelessWidget {
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
fieldInfo.name,
|
||||
lineHeight: 1.0,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
onTap: onTap,
|
||||
|
@ -34,6 +34,7 @@ class SortChoiceButton extends StatelessWidget {
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
text,
|
||||
lineHeight: 1.0,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
@ -222,6 +222,7 @@ class TabBarItemButton extends StatelessWidget {
|
||||
),
|
||||
text: FlowyText(
|
||||
view.name,
|
||||
lineHeight: 1.0,
|
||||
fontSize: FontSizes.s11,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
@ -290,4 +291,9 @@ enum TabBarViewAction implements ActionCell {
|
||||
|
||||
@override
|
||||
Widget? rightIcon(Color iconColor) => null;
|
||||
|
||||
@override
|
||||
Color? textColor(BuildContext context) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ extension DatabaseLayoutExtension on DatabaseLayoutPB {
|
||||
FlowySvgData get icon {
|
||||
return switch (this) {
|
||||
DatabaseLayoutPB.Board => FlowySvgs.board_s,
|
||||
DatabaseLayoutPB.Calendar => FlowySvgs.date_s,
|
||||
DatabaseLayoutPB.Calendar => FlowySvgs.calendar_s,
|
||||
DatabaseLayoutPB.Grid => FlowySvgs.grid_s,
|
||||
_ => throw UnimplementedError(),
|
||||
};
|
||||
|
@ -158,6 +158,7 @@ class _EditFieldButton extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
leftIcon: const FlowySvg(FlowySvgs.edit_s),
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.grid_field_editProperty.tr(),
|
||||
),
|
||||
onTap: onTap,
|
||||
@ -193,6 +194,7 @@ class FieldActionCell extends StatelessWidget {
|
||||
disable: !enable,
|
||||
text: FlowyText.medium(
|
||||
action.title(fieldInfo),
|
||||
lineHeight: 1.0,
|
||||
color: enable ? null : Theme.of(context).disabledColor,
|
||||
),
|
||||
onHover: (_) => popoverMutex?.close(),
|
||||
@ -613,6 +615,7 @@ class _SwitchFieldButtonState extends State<SwitchFieldButton> {
|
||||
},
|
||||
text: FlowyText.medium(
|
||||
state.field.fieldType.i18n,
|
||||
lineHeight: 1.0,
|
||||
color: isPrimary ? Theme.of(context).disabledColor : null,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
|
@ -75,9 +75,7 @@ class FieldTypeCell extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
fieldType.i18n,
|
||||
),
|
||||
text: FlowyText.medium(fieldType.i18n, lineHeight: 1.0),
|
||||
onTap: () => onSelectField(fieldType),
|
||||
leftIcon: FlowySvg(
|
||||
fieldType.svgData,
|
||||
|
@ -23,7 +23,10 @@ class DateFormatButton extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.grid_field_dateFormat.tr()),
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_dateFormat.tr(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onTap: onTap,
|
||||
onHover: onHover,
|
||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||
@ -47,7 +50,10 @@ class TimeFormatButton extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.grid_field_timeFormat.tr()),
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.grid_field_timeFormat.tr(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onTap: onTap,
|
||||
onHover: onHover,
|
||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||
@ -114,7 +120,10 @@ class DateFormatCell extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(dateFormat.title()),
|
||||
text: FlowyText.medium(
|
||||
dateFormat.title(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
rightIcon: checkmark,
|
||||
onTap: () => onSelected(dateFormat),
|
||||
),
|
||||
@ -199,7 +208,10 @@ class TimeFormatCell extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(timeFormat.title()),
|
||||
text: FlowyText.medium(
|
||||
timeFormat.title(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
rightIcon: checkmark,
|
||||
onTap: () => onSelected(timeFormat),
|
||||
),
|
||||
|
@ -32,6 +32,7 @@ class NumberTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
child: FlowyButton(
|
||||
rightIcon: const FlowySvg(FlowySvgs.more_s),
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
typeOption.format.title(),
|
||||
),
|
||||
),
|
||||
@ -167,7 +168,10 @@ class NumberFormatCell extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(format.title()),
|
||||
text: FlowyText.medium(
|
||||
format.title(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
onTap: () => onSelected(format),
|
||||
rightIcon: checkmark,
|
||||
),
|
||||
|
@ -61,6 +61,7 @@ class RelationTypeOptionEditorFactory implements TypeOptionEditorFactory {
|
||||
(meta) => meta.databaseId == typeOption.databaseId,
|
||||
);
|
||||
return FlowyText(
|
||||
lineHeight: 1.0,
|
||||
databaseMeta == null
|
||||
? LocaleKeys
|
||||
.grid_relation_relatedDatabasePlaceholder
|
||||
@ -134,6 +135,7 @@ class _DatabaseList extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
onTap: () => onSelectDatabase(meta.databaseId),
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
meta.databaseName,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
|
@ -181,6 +181,7 @@ class _AddOptionButton extends StatelessWidget {
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.grid_field_addSelectOption.tr(),
|
||||
),
|
||||
onTap: () {
|
||||
|
@ -107,6 +107,7 @@ class _DeleteTag extends StatelessWidget {
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.grid_selectOption_deleteTag.tr(),
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
||||
@ -230,6 +231,7 @@ class _SelectOptionColorCell extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
color.colorName(),
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
|
@ -97,7 +97,12 @@ class SelectLanguageButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(text: FlowyText(language)),
|
||||
child: FlowyButton(
|
||||
text: FlowyText(
|
||||
language,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -159,7 +164,10 @@ class LanguageCell extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(languageTypeToLanguage(languageType)),
|
||||
text: FlowyText.medium(
|
||||
languageTypeToLanguage(languageType),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
rightIcon: checkmark,
|
||||
onTap: () => onSelected(languageType),
|
||||
),
|
||||
|
@ -193,6 +193,7 @@ class _GridGroupCell extends StatelessWidget {
|
||||
text: FlowyText.medium(
|
||||
name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
leftIcon: icon != null
|
||||
? FlowySvg(
|
||||
|
@ -50,7 +50,10 @@ class RowDetailPageDeleteButton extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(LocaleKeys.grid_row_delete.tr()),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.grid_row_delete.tr(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.trash_m),
|
||||
onTap: () {
|
||||
RowBackendService.deleteRows(viewId, [rowId]);
|
||||
@ -76,7 +79,10 @@ class RowDetailPageDuplicateButton extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: GridSize.popoverItemHeight,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.regular(LocaleKeys.grid_row_duplicate.tr()),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.grid_row_duplicate.tr(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.copy_s),
|
||||
onTap: () {
|
||||
RowBackendService.duplicateRow(viewId, rowId);
|
||||
|
@ -220,6 +220,7 @@ class AddEmojiButton extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.document_plugins_cover_addIcon.tr(),
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
||||
@ -242,6 +243,7 @@ class RemoveEmojiButton extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.document_plugins_cover_removeIcon.tr(),
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.emoji_s),
|
||||
|
@ -294,7 +294,11 @@ class ToggleHiddenFieldsVisibilityButton extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: 30,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(text, color: Theme.of(context).hintColor),
|
||||
text: FlowyText.medium(
|
||||
text,
|
||||
lineHeight: 1.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
leftIcon: RotatedBox(
|
||||
quarterTurns: quarterTurns,
|
||||
@ -381,6 +385,7 @@ class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 6),
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.grid_field_newProperty.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
|
@ -80,6 +80,7 @@ class DatabaseViewLayoutCell extends StatelessWidget {
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
databaseLayout.layoutName,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
|
@ -23,7 +23,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction {
|
||||
FlowySvgData iconData() {
|
||||
switch (this) {
|
||||
case DatabaseSettingAction.showProperties:
|
||||
return FlowySvgs.properties_s;
|
||||
return FlowySvgs.multiselect_s;
|
||||
case DatabaseSettingAction.showLayout:
|
||||
return FlowySvgs.database_layout_m;
|
||||
case DatabaseSettingAction.showGroup:
|
||||
@ -82,6 +82,7 @@ extension DatabaseSettingActionExtension on DatabaseSettingAction {
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
title(),
|
||||
lineHeight: 1.0,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
leftIcon: FlowySvg(
|
||||
|
@ -150,6 +150,7 @@ class _DatabasePropertyCellState extends State<DatabasePropertyCell> {
|
||||
child: FlowyButton(
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
text: FlowyText.medium(
|
||||
lineHeight: 1.0,
|
||||
widget.fieldInfo.name,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
|
@ -189,7 +189,10 @@ Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
),
|
||||
),
|
||||
CalloutBlockKeys.type: CalloutBlockComponentBuilder(
|
||||
configuration: configuration.copyWith(),
|
||||
configuration: configuration.copyWith(
|
||||
padding: (node) => const EdgeInsets.symmetric(vertical: 10),
|
||||
),
|
||||
inlinePadding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
defaultColor: calloutBGColor,
|
||||
),
|
||||
DividerBlockKeys.type: DividerBlockComponentBuilder(
|
||||
|
@ -154,6 +154,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
// callout block
|
||||
insertNewLineInCalloutBlock,
|
||||
|
||||
// quote block
|
||||
insertNewLineInQuoteBlock,
|
||||
|
||||
// toggle list
|
||||
formatGreaterToToggleList,
|
||||
insertChildNodeInsideToggleList,
|
||||
|
@ -20,6 +20,7 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
this.title,
|
||||
this.showBorder = true,
|
||||
this.enable = true,
|
||||
this.margin,
|
||||
});
|
||||
|
||||
final String emoji;
|
||||
@ -33,6 +34,7 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
final String? title;
|
||||
final bool showBorder;
|
||||
final bool enable;
|
||||
final EdgeInsets? margin;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@ -44,6 +46,7 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
height: emojiPickerSize.height,
|
||||
),
|
||||
offset: offset,
|
||||
margin: EdgeInsets.zero,
|
||||
direction: direction ?? PopoverDirection.rightWithTopAligned,
|
||||
popupBuilder: (_) => Container(
|
||||
width: emojiPickerSize.width,
|
||||
@ -79,15 +82,16 @@ class EmojiPickerButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return FlowyTextButton(
|
||||
emoji,
|
||||
overflow: TextOverflow.visible,
|
||||
fontSize: emojiSize,
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints.tightFor(width: 36.0),
|
||||
fillColor: Colors.transparent,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
onPressed: enable
|
||||
return FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
margin:
|
||||
margin ?? const EdgeInsets.symmetric(horizontal: 8.0, vertical: 6.0),
|
||||
text: FlowyText.emoji(
|
||||
emoji,
|
||||
fontSize: emojiSize,
|
||||
optimizeEmojiAlign: true,
|
||||
),
|
||||
onTap: enable
|
||||
? () async {
|
||||
final result = await context.push<EmojiPickerResult>(
|
||||
Uri(
|
||||
|
@ -53,7 +53,10 @@ class SelectableItem extends StatelessWidget {
|
||||
return SizedBox(
|
||||
height: 32,
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(item),
|
||||
text: FlowyText.medium(
|
||||
item,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
rightIcon: isSelected ? const FlowySvg(FlowySvgs.check_s) : null,
|
||||
onTap: onTap,
|
||||
),
|
||||
|
@ -68,9 +68,11 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder {
|
||||
CalloutBlockComponentBuilder({
|
||||
super.configuration,
|
||||
required this.defaultColor,
|
||||
required this.inlinePadding,
|
||||
});
|
||||
|
||||
final Color defaultColor;
|
||||
final EdgeInsets inlinePadding;
|
||||
|
||||
@override
|
||||
BlockComponentWidget build(BlockComponentContext blockComponentContext) {
|
||||
@ -79,6 +81,7 @@ class CalloutBlockComponentBuilder extends BlockComponentBuilder {
|
||||
key: node.key,
|
||||
node: node,
|
||||
defaultColor: defaultColor,
|
||||
inlinePadding: inlinePadding,
|
||||
configuration: configuration,
|
||||
showActions: showActions(node),
|
||||
actionBuilder: (context, state) => actionBuilder(
|
||||
@ -105,9 +108,11 @@ class CalloutBlockComponentWidget extends BlockComponentStatefulWidget {
|
||||
super.actionBuilder,
|
||||
super.configuration = const BlockComponentConfiguration(),
|
||||
required this.defaultColor,
|
||||
required this.inlinePadding,
|
||||
});
|
||||
|
||||
final Color defaultColor;
|
||||
final EdgeInsets inlinePadding;
|
||||
|
||||
@override
|
||||
State<CalloutBlockComponentWidget> createState() =>
|
||||
@ -176,6 +181,7 @@ class _CalloutBlockComponentWidgetState
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
color: backgroundColor,
|
||||
),
|
||||
padding: widget.inlinePadding,
|
||||
width: double.infinity,
|
||||
alignment: alignment,
|
||||
child: Row(
|
||||
@ -183,27 +189,22 @@ class _CalloutBlockComponentWidgetState
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
textDirection: textDirection,
|
||||
children: [
|
||||
if (PlatformExtension.isDesktopOrWeb) const HSpace(4.0),
|
||||
// the emoji picker button for the note
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 6.0,
|
||||
left: 4.0,
|
||||
right: 4.0,
|
||||
),
|
||||
child: EmojiPickerButton(
|
||||
key: ValueKey(
|
||||
emoji.toString(),
|
||||
), // force to refresh the popover state
|
||||
enable: editorState.editable,
|
||||
title: '',
|
||||
emoji: emoji,
|
||||
emojiSize: 16.0,
|
||||
onSubmitted: (emoji, controller) {
|
||||
setEmoji(emoji);
|
||||
controller?.close();
|
||||
},
|
||||
),
|
||||
EmojiPickerButton(
|
||||
key: ValueKey(
|
||||
emoji.toString(),
|
||||
), // force to refresh the popover state
|
||||
enable: editorState.editable,
|
||||
title: '',
|
||||
emoji: emoji,
|
||||
emojiSize: 15.0,
|
||||
onSubmitted: (emoji, controller) {
|
||||
setEmoji(emoji);
|
||||
controller?.close();
|
||||
},
|
||||
),
|
||||
if (PlatformExtension.isDesktopOrWeb) const HSpace(4.0),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 4.0),
|
||||
@ -248,24 +249,21 @@ class _CalloutBlockComponentWidgetState
|
||||
BuildContext context,
|
||||
TextDirection textDirection,
|
||||
) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
delegate: this,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
textStyle,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
placeholderTextStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
return AppFlowyRichText(
|
||||
key: forwardKey,
|
||||
delegate: this,
|
||||
node: widget.node,
|
||||
editorState: editorState,
|
||||
placeholderText: placeholderText,
|
||||
textSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
textStyle,
|
||||
),
|
||||
placeholderTextSpanDecorator: (textSpan) => textSpan.updateTextStyle(
|
||||
placeholderTextStyle,
|
||||
),
|
||||
textDirection: textDirection,
|
||||
cursorColor: editorState.editorStyle.cursorColor,
|
||||
selectionColor: editorState.editorStyle.selectionColor,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_image.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_in_app_json.dart';
|
||||
|
@ -1,188 +0,0 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
final _listTypes = [
|
||||
BulletedListBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
NumberedListBlockKeys.type,
|
||||
];
|
||||
|
||||
extension PasteNodes on EditorState {
|
||||
Future<void> pasteSingleLineNode(Node insertedNode) async {
|
||||
final selection = await deleteSelectionIfNeeded();
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
final node = getNodeAtPath(selection.start.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
return;
|
||||
}
|
||||
final transaction = this.transaction;
|
||||
final insertedDelta = insertedNode.delta;
|
||||
// if the node is empty and its type is paragprah, replace it with the inserted node.
|
||||
if (delta.isEmpty && node.type == ParagraphBlockKeys.type) {
|
||||
transaction.insertNode(
|
||||
selection.end.path.next,
|
||||
insertedNode,
|
||||
);
|
||||
transaction.deleteNode(node);
|
||||
final path = calculatePath(selection.end.path, [insertedNode]);
|
||||
final offset = calculateLength([insertedNode]);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: path,
|
||||
offset: offset,
|
||||
),
|
||||
);
|
||||
} else if (_listTypes.contains(node.type)) {
|
||||
final convertedNode = insertedNode.copyWith(type: node.type);
|
||||
final path = selection.start.path;
|
||||
transaction
|
||||
..insertNode(path, convertedNode)
|
||||
..deleteNodesAtPath(path);
|
||||
|
||||
// Set the afterSelection to the last child of the inserted node
|
||||
final lastChildPath = calculatePath(path, [convertedNode]);
|
||||
final lastChildOffset = calculateLength([convertedNode]);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(path: lastChildPath, offset: lastChildOffset),
|
||||
);
|
||||
} else if (insertedDelta != null) {
|
||||
// if the node is not empty, insert the delta from inserted node after the selection.
|
||||
transaction.insertTextDelta(node, selection.endIndex, insertedDelta);
|
||||
}
|
||||
await apply(transaction);
|
||||
}
|
||||
|
||||
Future<void> pasteMultiLineNodes(List<Node> nodes) async {
|
||||
assert(nodes.length > 1);
|
||||
|
||||
final selection = await deleteSelectionIfNeeded();
|
||||
if (selection == null) {
|
||||
return;
|
||||
}
|
||||
final node = getNodeAtPath(selection.start.path);
|
||||
final delta = node?.delta;
|
||||
if (node == null || delta == null) {
|
||||
return;
|
||||
}
|
||||
final transaction = this.transaction;
|
||||
|
||||
final lastNodeLength = calculateLength(nodes);
|
||||
// merge the current selected node delta into the nodes.
|
||||
if (delta.isNotEmpty) {
|
||||
nodes.first.insertDelta(
|
||||
delta.slice(0, selection.startIndex),
|
||||
insertAfter: false,
|
||||
);
|
||||
|
||||
nodes.last.insertDelta(
|
||||
delta.slice(selection.endIndex),
|
||||
);
|
||||
}
|
||||
|
||||
if (delta.isEmpty && node.type != ParagraphBlockKeys.type) {
|
||||
nodes[0] = nodes.first.copyWith(
|
||||
type: node.type,
|
||||
attributes: {
|
||||
...node.attributes,
|
||||
...nodes.first.attributes,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
for (final child in node.children) {
|
||||
nodes.last.insert(child);
|
||||
}
|
||||
|
||||
transaction.insertNodes(selection.end.path, nodes);
|
||||
|
||||
// delete the current node.
|
||||
transaction.deleteNode(node);
|
||||
|
||||
final path = calculatePath(selection.start.path, nodes);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(
|
||||
path: path,
|
||||
offset: lastNodeLength,
|
||||
),
|
||||
);
|
||||
|
||||
await apply(transaction);
|
||||
}
|
||||
|
||||
// delete the selection if it's not collapsed.
|
||||
Future<Selection?> deleteSelectionIfNeeded() async {
|
||||
final selection = this.selection;
|
||||
if (selection == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// delete the selection first.
|
||||
if (!selection.isCollapsed) {
|
||||
await deleteSelection(selection);
|
||||
}
|
||||
|
||||
// fetch selection again.selection = editorState.selection;
|
||||
assert(this.selection?.isCollapsed == true);
|
||||
return this.selection;
|
||||
}
|
||||
|
||||
Path calculatePath(Path start, List<Node> nodes) {
|
||||
var path = start;
|
||||
for (var i = 0; i < nodes.length; i++) {
|
||||
path = path.next;
|
||||
}
|
||||
path = path.previous;
|
||||
if (nodes.last.children.isNotEmpty) {
|
||||
return [
|
||||
...path,
|
||||
...calculatePath([0], nodes.last.children.toList()),
|
||||
];
|
||||
}
|
||||
return path;
|
||||
}
|
||||
|
||||
int calculateLength(List<Node> nodes) {
|
||||
if (nodes.last.children.isNotEmpty) {
|
||||
return calculateLength(nodes.last.children.toList());
|
||||
}
|
||||
return nodes.last.delta?.length ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
extension on Node {
|
||||
void insertDelta(Delta delta, {bool insertAfter = true}) {
|
||||
assert(delta.every((element) => element is TextInsert));
|
||||
if (this.delta == null) {
|
||||
updateAttributes({
|
||||
blockComponentDelta: delta.toJson(),
|
||||
});
|
||||
} else if (insertAfter) {
|
||||
updateAttributes(
|
||||
{
|
||||
blockComponentDelta: this
|
||||
.delta!
|
||||
.compose(
|
||||
Delta()
|
||||
..retain(this.delta!.length)
|
||||
..addAll(delta),
|
||||
)
|
||||
.toJson(),
|
||||
},
|
||||
);
|
||||
} else {
|
||||
updateAttributes(
|
||||
{
|
||||
blockComponentDelta: delta
|
||||
.compose(
|
||||
Delta()
|
||||
..retain(delta.length)
|
||||
..addAll(this.delta!),
|
||||
)
|
||||
.toJson(),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
extension PasteFromHtml on EditorState {
|
||||
|
@ -1,6 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
|
@ -84,7 +84,7 @@ class _DatabaseBlockComponentWidgetState
|
||||
child: FocusScope(
|
||||
skipTraversal: true,
|
||||
onFocusChange: (value) {
|
||||
if (value) {
|
||||
if (value && keepEditorFocusNotifier.value == 0) {
|
||||
context.read<EditorState>().selection = null;
|
||||
}
|
||||
},
|
||||
|
@ -208,7 +208,11 @@ class FileBlockComponentState extends State<FileBlockComponent>
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(10),
|
||||
const Icon(Icons.upload_file_outlined),
|
||||
FlowySvg(
|
||||
FlowySvgs.slash_menu_icon_file_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(24),
|
||||
),
|
||||
const HSpace(10),
|
||||
..._buildTrailing(context),
|
||||
],
|
||||
@ -348,6 +352,7 @@ class FileBlockComponentState extends State<FileBlockComponent>
|
||||
? LocaleKeys.document_plugins_file_placeholderDragging.tr()
|
||||
: LocaleKeys.document_plugins_file_placeholderText.tr(),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
@ -142,7 +142,7 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
||||
borderType: BorderType.RRect,
|
||||
color: isDragging
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Colors.black,
|
||||
: Theme.of(context).hintColor,
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@ -153,7 +153,7 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
||||
LocaleKeys.document_plugins_file_dropFileToUpload
|
||||
.tr(),
|
||||
fontSize: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const VSpace(13.5),
|
||||
] else ...[
|
||||
@ -162,8 +162,9 @@ class _FileUploadLocalState extends State<_FileUploadLocal> {
|
||||
.tr(),
|
||||
fontSize: 16,
|
||||
maxLines: 2,
|
||||
textAlign: TextAlign.center,
|
||||
lineHeight: 1.5,
|
||||
textAlign: TextAlign.center,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
],
|
||||
@ -208,6 +209,7 @@ class _FileUploadNetworkState extends State<_FileUploadNetwork> {
|
||||
alignment: Alignment.center,
|
||||
child: Column(
|
||||
children: [
|
||||
const VSpace(12),
|
||||
FlowyTextField(
|
||||
hintText: LocaleKeys.document_plugins_file_networkHint.tr(),
|
||||
onChanged: (value) => inputText = value,
|
||||
@ -220,19 +222,25 @@ class _FileUploadNetworkState extends State<_FileUploadNetwork> {
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
const VSpace(8),
|
||||
const VSpace(20),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 32,
|
||||
width: 300,
|
||||
child: FlowyButton(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
hoverColor:
|
||||
Theme.of(context).colorScheme.primary.withOpacity(0.9),
|
||||
showDefaultBoxDecorationOnMobile: true,
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
margin: const EdgeInsets.all(5),
|
||||
text: FlowyText(
|
||||
LocaleKeys.document_plugins_file_networkAction.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onTap: submit,
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -247,6 +247,7 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
|
||||
size: Size(20, 20),
|
||||
),
|
||||
text: FlowyText(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
|
||||
),
|
||||
),
|
||||
|
@ -71,9 +71,10 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(10),
|
||||
const FlowySvg(
|
||||
FlowySvgs.image_placeholder_s,
|
||||
size: Size.square(24),
|
||||
FlowySvg(
|
||||
FlowySvgs.slash_menu_icon_image_s,
|
||||
size: const Size.square(24),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
const HSpace(10),
|
||||
..._buildTrailing(context),
|
||||
@ -101,7 +102,6 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
UploadImageType.local,
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
UploadImageType.stabilityAI,
|
||||
],
|
||||
onSelectedLocalImages: (paths) {
|
||||
controller.close();
|
||||
@ -192,6 +192,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
? LocaleKeys.document_plugins_image_dropImageToInsert.tr()
|
||||
: LocaleKeys.document_plugins_image_addAnImageDesktop.tr()
|
||||
: LocaleKeys.document_plugins_image_addAnImageMobile.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
];
|
||||
|
@ -1,8 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
@ -25,6 +22,8 @@ import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:provider/provider.dart';
|
||||
@ -129,7 +128,7 @@ class _MultiImageMenuState extends State<MultiImageMenu> {
|
||||
UploadImageType.local,
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
UploadImageType.stabilityAI,
|
||||
|
||||
],
|
||||
onSelectedLocalImages: insertLocalImages,
|
||||
onSelectedAIImage: insertAIImage,
|
||||
|
@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
@ -23,6 +22,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@ -66,7 +66,11 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.photo_library_outlined, size: 24),
|
||||
FlowySvg(
|
||||
FlowySvgs.slash_menu_icon_photo_gallery_s,
|
||||
color: Theme.of(context).hintColor,
|
||||
size: const Size.square(24),
|
||||
),
|
||||
const HSpace(10),
|
||||
FlowyText(
|
||||
PlatformExtension.isDesktop
|
||||
@ -76,6 +80,7 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
|
||||
: LocaleKeys.document_plugins_image_addAnImageDesktop
|
||||
.tr()
|
||||
: LocaleKeys.document_plugins_image_addAnImageMobile.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
),
|
||||
@ -101,7 +106,6 @@ class MultiImagePlaceholderState extends State<MultiImagePlaceholder> {
|
||||
UploadImageType.local,
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
UploadImageType.stabilityAI,
|
||||
],
|
||||
onSelectedLocalImages: (paths) {
|
||||
controller.close();
|
||||
|
@ -1,17 +1,15 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/stability_ai_image_widget.dart';
|
||||
//import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/stability_ai_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/upload_image_file_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide ColorOption;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'widgets/embed_image_url_widget.dart';
|
||||
|
||||
@ -19,8 +17,6 @@ enum UploadImageType {
|
||||
local,
|
||||
url,
|
||||
unsplash,
|
||||
stabilityAI,
|
||||
// openAI,
|
||||
color;
|
||||
|
||||
String get description {
|
||||
@ -31,10 +27,6 @@ enum UploadImageType {
|
||||
return LocaleKeys.document_imageBlock_embedLink_label.tr();
|
||||
case UploadImageType.unsplash:
|
||||
return LocaleKeys.document_imageBlock_unsplash_label.tr();
|
||||
// case UploadImageType.openAI:
|
||||
// return LocaleKeys.document_imageBlock_ai_label.tr();
|
||||
case UploadImageType.stabilityAI:
|
||||
return LocaleKeys.document_imageBlock_stability_ai_label.tr();
|
||||
case UploadImageType.color:
|
||||
return LocaleKeys.document_plugins_cover_colors.tr();
|
||||
}
|
||||
@ -68,33 +60,12 @@ class UploadImageMenu extends StatefulWidget {
|
||||
class _UploadImageMenuState extends State<UploadImageMenu> {
|
||||
late final List<UploadImageType> values;
|
||||
int currentTabIndex = 0;
|
||||
bool supportOpenAI = false;
|
||||
bool supportStabilityAI = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
values = widget.supportTypes;
|
||||
UserBackendService.getCurrentUserProfile().then(
|
||||
(value) {
|
||||
final supportOpenAI = value.fold(
|
||||
(s) => s.openaiKey.isNotEmpty,
|
||||
(e) => false,
|
||||
);
|
||||
final supportStabilityAI = value.fold(
|
||||
(s) => s.stabilityAiKey.isNotEmpty,
|
||||
(e) => false,
|
||||
);
|
||||
if (supportOpenAI != this.supportOpenAI ||
|
||||
supportStabilityAI != this.supportStabilityAI) {
|
||||
setState(() {
|
||||
this.supportOpenAI = supportOpenAI;
|
||||
this.supportStabilityAI = supportStabilityAI;
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@ -150,26 +121,37 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
|
||||
final type = values[currentTabIndex];
|
||||
switch (type) {
|
||||
case UploadImageType.local:
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
alignment: Alignment.center,
|
||||
constraints: constraints,
|
||||
child: Column(
|
||||
children: [
|
||||
UploadImageFileWidget(
|
||||
allowMultipleImages: widget.allowMultipleImages,
|
||||
onPickFiles: widget.onSelectedLocalImages,
|
||||
),
|
||||
if (widget.limitMaximumImageSize) ...[
|
||||
const VSpace(6.0),
|
||||
FlowyText(
|
||||
LocaleKeys.document_imageBlock_maximumImageSize.tr(),
|
||||
fontSize: 12.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
return Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Container(
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
constraints: constraints,
|
||||
child: Column(
|
||||
children: [
|
||||
UploadImageFileWidget(
|
||||
allowMultipleImages: widget.allowMultipleImages,
|
||||
onPickFiles: widget.onSelectedLocalImages,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// if (widget.limitMaximumImageSize) ...[
|
||||
// FlowyText(
|
||||
// LocaleKeys.document_imageBlock_maximumImageSize.tr(),
|
||||
// fontSize: 10.0,
|
||||
// color: Theme.of(context).hintColor,
|
||||
// ),
|
||||
// ],
|
||||
],
|
||||
);
|
||||
case UploadImageType.url:
|
||||
return Container(
|
||||
@ -188,23 +170,6 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
|
||||
),
|
||||
),
|
||||
);
|
||||
case UploadImageType.stabilityAI:
|
||||
return supportStabilityAI
|
||||
? Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: StabilityAIImageWidget(
|
||||
onSelectImage: (url) => widget.onSelectedLocalImages([url]),
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.document_imageBlock_pleaseInputYourStabilityAIKey
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
case UploadImageType.color:
|
||||
final theme = Theme.of(context);
|
||||
final padding = PlatformExtension.isMobile
|
||||
|
@ -1,9 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/patterns/common_patterns.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class EmbedImageUrlWidget extends StatefulWidget {
|
||||
const EmbedImageUrlWidget({
|
||||
@ -25,31 +24,38 @@ class _EmbedImageUrlWidgetState extends State<EmbedImageUrlWidget> {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const VSpace(12),
|
||||
FlowyTextField(
|
||||
hintText: LocaleKeys.document_imageBlock_embedLink_placeholder.tr(),
|
||||
onChanged: (value) => inputText = value,
|
||||
onEditingComplete: submit,
|
||||
),
|
||||
if (!isUrlValid) ...[
|
||||
const VSpace(8),
|
||||
const VSpace(12),
|
||||
FlowyText(
|
||||
LocaleKeys.document_plugins_cover_invalidImageUrl.tr(),
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
const VSpace(8),
|
||||
const VSpace(20),
|
||||
SizedBox(
|
||||
width: 160,
|
||||
height: 32,
|
||||
width: 300,
|
||||
child: FlowyButton(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.9),
|
||||
showDefaultBoxDecorationOnMobile: true,
|
||||
margin: const EdgeInsets.all(8.0),
|
||||
margin: const EdgeInsets.all(5),
|
||||
text: FlowyText(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
lineHeight: 1,
|
||||
textAlign: TextAlign.center,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
onTap: submit,
|
||||
),
|
||||
),
|
||||
const VSpace(8),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
@ -1,105 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
|
||||
class OpenAIImageWidget extends StatefulWidget {
|
||||
const OpenAIImageWidget({
|
||||
super.key,
|
||||
required this.onSelectNetworkImage,
|
||||
});
|
||||
|
||||
final void Function(String url) onSelectNetworkImage;
|
||||
|
||||
@override
|
||||
State<OpenAIImageWidget> createState() => _OpenAIImageWidgetState();
|
||||
}
|
||||
|
||||
class _OpenAIImageWidgetState extends State<OpenAIImageWidget> {
|
||||
Future<FlowyResult<List<String>, AIError>>? future;
|
||||
String query = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyTextField(
|
||||
hintText: LocaleKeys.document_imageBlock_ai_placeholder.tr(),
|
||||
onChanged: (value) => query = value,
|
||||
onEditingComplete: _search,
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.search_label.tr(),
|
||||
),
|
||||
onTap: _search,
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(12.0),
|
||||
if (future != null)
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, value) {
|
||||
final data = value.data;
|
||||
if (!value.hasData ||
|
||||
value.connectionState != ConnectionState.done ||
|
||||
data == null) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
}
|
||||
return data.fold(
|
||||
(s) => GridView.count(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 16.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
childAspectRatio: 4 / 3,
|
||||
children: s
|
||||
.map(
|
||||
(e) => GestureDetector(
|
||||
onTap: () => widget.onSelectNetworkImage(e),
|
||||
child: Image.network(e),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
(e) => Center(
|
||||
child: FlowyText(
|
||||
e.message,
|
||||
maxLines: 3,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _search() async {
|
||||
final openAI = await getIt.getAsync<AIRepository>();
|
||||
setState(() {
|
||||
future = openAI.generateImage(
|
||||
prompt: query,
|
||||
n: 6,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -1,120 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
class StabilityAIImageWidget extends StatefulWidget {
|
||||
const StabilityAIImageWidget({
|
||||
super.key,
|
||||
required this.onSelectImage,
|
||||
});
|
||||
|
||||
final void Function(String url) onSelectImage;
|
||||
|
||||
@override
|
||||
State<StabilityAIImageWidget> createState() => _StabilityAIImageWidgetState();
|
||||
}
|
||||
|
||||
class _StabilityAIImageWidgetState extends State<StabilityAIImageWidget> {
|
||||
Future<FlowyResult<List<String>, StabilityAIRequestError>>? future;
|
||||
String query = '';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FlowyTextField(
|
||||
hintText: LocaleKeys
|
||||
.document_imageBlock_stability_ai_placeholder
|
||||
.tr(),
|
||||
onChanged: (value) => query = value,
|
||||
onEditingComplete: _search,
|
||||
),
|
||||
),
|
||||
const HSpace(4.0),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText(
|
||||
LocaleKeys.search_label.tr(),
|
||||
),
|
||||
onTap: _search,
|
||||
),
|
||||
],
|
||||
),
|
||||
const VSpace(12.0),
|
||||
if (future != null)
|
||||
Expanded(
|
||||
child: FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, value) {
|
||||
final data = value.data;
|
||||
if (!value.hasData ||
|
||||
value.connectionState != ConnectionState.done ||
|
||||
data == null) {
|
||||
return const CircularProgressIndicator.adaptive();
|
||||
}
|
||||
return data.fold(
|
||||
(s) => GridView.count(
|
||||
crossAxisCount: 3,
|
||||
mainAxisSpacing: 16.0,
|
||||
crossAxisSpacing: 10.0,
|
||||
childAspectRatio: 4 / 3,
|
||||
children: s.map(
|
||||
(e) {
|
||||
final base64Image = base64Decode(e);
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final path = p.join(
|
||||
tempDirectory.path,
|
||||
'${uuid()}.png',
|
||||
);
|
||||
File(path).writeAsBytesSync(base64Image);
|
||||
widget.onSelectImage(path);
|
||||
},
|
||||
child: Image.memory(base64Image),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
(e) => Center(
|
||||
child: FlowyText(
|
||||
e.message,
|
||||
maxLines: 3,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
void _search() async {
|
||||
final stabilityAI = await getIt.getAsync<StabilityAIRepository>();
|
||||
setState(() {
|
||||
future = stabilityAI.generateImage(
|
||||
prompt: query,
|
||||
n: 6,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
@ -173,11 +173,12 @@ class MathEquationBlockComponentWidgetState
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(10),
|
||||
const Icon(Icons.text_fields_outlined),
|
||||
FlowySvg(FlowySvgs.slash_menu_icon_math_equation_s,
|
||||
color: Theme.of(context).hintColor, size: const Size.square(24),),
|
||||
const HSpace(10),
|
||||
FlowyText(
|
||||
LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
|
||||
),
|
||||
LocaleKeys.document_plugins_mathEquation_addMathEquation.tr(),
|
||||
color: Theme.of(context).hintColor,),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
@ -21,6 +22,7 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
TextTransaction,
|
||||
paragraphNode;
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -105,57 +107,69 @@ class _MentionPageBlockState extends State<MentionPageBlock> {
|
||||
// memorize the result
|
||||
pageMemorizer[widget.pageId] = view;
|
||||
if (view == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final iconSize = widget.textStyle?.fontSize ?? 16.0;
|
||||
final child = GestureDetector(
|
||||
onTap: handleTap,
|
||||
onDoubleTap: handleDoubleTap,
|
||||
behavior: HitTestBehavior.translucent,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const HSpace(4),
|
||||
view.icon.value.isNotEmpty
|
||||
? EmojiText(
|
||||
emoji: view.icon.value,
|
||||
fontSize: 12,
|
||||
textAlign: TextAlign.center,
|
||||
lineHeight: 1.3,
|
||||
)
|
||||
: FlowySvg(
|
||||
view.layout.icon,
|
||||
size: Size.square(iconSize + 2.0),
|
||||
),
|
||||
const HSpace(2),
|
||||
FlowyText(
|
||||
view.name,
|
||||
return FlowyHover(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: FlowyText(
|
||||
LocaleKeys.document_mention_noAccess.tr(),
|
||||
color: Theme.of(context).disabledColor,
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: widget.textStyle?.fontSize,
|
||||
fontWeight: widget.textStyle?.fontWeight,
|
||||
),
|
||||
const HSpace(2),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (PlatformExtension.isMobile) {
|
||||
return child;
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: FlowyHover(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: child,
|
||||
),
|
||||
final iconSize = widget.textStyle?.fontSize ?? 16.0;
|
||||
Widget child = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const HSpace(4),
|
||||
view.icon.value.isNotEmpty
|
||||
? EmojiText(
|
||||
emoji: view.icon.value,
|
||||
fontSize: 12,
|
||||
textAlign: TextAlign.center,
|
||||
lineHeight: 1.3,
|
||||
)
|
||||
: FlowySvg(
|
||||
view.layout.icon,
|
||||
size: Size.square(iconSize + 2.0),
|
||||
),
|
||||
const HSpace(2),
|
||||
FlowyText(
|
||||
view.name,
|
||||
decoration: TextDecoration.underline,
|
||||
fontSize: widget.textStyle?.fontSize,
|
||||
fontWeight: widget.textStyle?.fontWeight,
|
||||
),
|
||||
const HSpace(4),
|
||||
],
|
||||
);
|
||||
|
||||
if (PlatformExtension.isDesktop) {
|
||||
child = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 2),
|
||||
child: FlowyHover(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return GestureDetector(
|
||||
onTap: handleTap,
|
||||
onDoubleTap: PlatformExtension.isMobile ? handleDoubleTap : null,
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> handleTap() async {
|
||||
debugPrint('handleTap');
|
||||
final view = await fetchView(widget.pageId);
|
||||
if (view == null) {
|
||||
Log.error('Page(${widget.pageId}) not found');
|
||||
|
@ -51,6 +51,7 @@ export 'openai/widgets/smart_edit_node_widget.dart';
|
||||
export 'openai/widgets/smart_edit_toolbar_item.dart';
|
||||
export 'outline/outline_block_component.dart';
|
||||
export 'parsers/markdown_parsers.dart';
|
||||
export 'quote/quote_block_shortcuts.dart';
|
||||
export 'slash_menu/slash_menu_items.dart';
|
||||
export 'table/table_menu.dart';
|
||||
export 'table/table_option_action.dart';
|
||||
|
@ -0,0 +1,39 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
/// Pressing Enter in a quote block will insert a newline (\n) within the quote,
|
||||
/// while pressing Shift+Enter in a quote will insert a new paragraph next to the quote.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - mobile
|
||||
/// - web
|
||||
///
|
||||
final CharacterShortcutEvent insertNewLineInQuoteBlock = CharacterShortcutEvent(
|
||||
key: 'insert a new line in quote block',
|
||||
character: '\n',
|
||||
handler: _insertNewLineHandler,
|
||||
);
|
||||
|
||||
CharacterShortcutEventHandler _insertNewLineHandler = (editorState) async {
|
||||
final selection = editorState.selection?.normalized;
|
||||
if (selection == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final node = editorState.getNodeAtPath(selection.start.path);
|
||||
if (node == null || node.type != QuoteBlockKeys.type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// delete the selection
|
||||
await editorState.deleteSelection(selection);
|
||||
|
||||
if (HardwareKeyboard.instance.isShiftPressed) {
|
||||
await editorState.insertNewLine();
|
||||
} else {
|
||||
await editorState.insertTextAtCurrentSelection('\n');
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
@ -448,7 +448,7 @@ Widget _slashMenuItemNameBuilder(
|
||||
SelectionMenuStyle style,
|
||||
bool isSelected,
|
||||
) {
|
||||
return FlowyText(
|
||||
return FlowyText.regular(
|
||||
name,
|
||||
fontSize: 12.0,
|
||||
figmaLineHeight: 15.0,
|
||||
|
@ -1,95 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_error.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
enum StabilityAIRequestType {
|
||||
imageGenerations;
|
||||
|
||||
Uri get uri {
|
||||
switch (this) {
|
||||
case StabilityAIRequestType.imageGenerations:
|
||||
return Uri.parse(
|
||||
'https://api.stability.ai/v1/generation/stable-diffusion-xl-1024-v1-0/text-to-image',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract class StabilityAIRepository {
|
||||
/// Generate image from Stability AI
|
||||
///
|
||||
/// [prompt] is the prompt text
|
||||
/// [n] is the number of images to generate
|
||||
///
|
||||
/// the return value is a list of base64 encoded images
|
||||
Future<FlowyResult<List<String>, StabilityAIRequestError>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpStabilityAIRepository implements StabilityAIRepository {
|
||||
const HttpStabilityAIRepository({
|
||||
required this.client,
|
||||
required this.apiKey,
|
||||
});
|
||||
|
||||
final http.Client client;
|
||||
final String apiKey;
|
||||
|
||||
Map<String, String> get headers => {
|
||||
'Authorization': 'Bearer $apiKey',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
@override
|
||||
Future<FlowyResult<List<String>, StabilityAIRequestError>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
}) async {
|
||||
final parameters = {
|
||||
'text_prompts': [
|
||||
{
|
||||
'text': prompt,
|
||||
}
|
||||
],
|
||||
'samples': n,
|
||||
};
|
||||
|
||||
try {
|
||||
final response = await client.post(
|
||||
StabilityAIRequestType.imageGenerations.uri,
|
||||
headers: headers,
|
||||
body: json.encode(parameters),
|
||||
);
|
||||
|
||||
final data = json.decode(
|
||||
utf8.decode(response.bodyBytes),
|
||||
);
|
||||
if (response.statusCode == 200) {
|
||||
final artifacts = data['artifacts'] as List;
|
||||
final base64Images = artifacts
|
||||
.map(
|
||||
(e) => e['base64'].toString(),
|
||||
)
|
||||
.toList();
|
||||
return FlowyResult.success(base64Images);
|
||||
} else {
|
||||
return FlowyResult.failure(
|
||||
StabilityAIRequestError(
|
||||
data['message'].toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return FlowyResult.failure(
|
||||
StabilityAIRequestError(
|
||||
error.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
class StabilityAIRequestError {
|
||||
StabilityAIRequestError(this.message);
|
||||
|
||||
final String message;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'StabilityAIRequestError{message: $message}';
|
||||
}
|
||||
}
|
@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
@ -7,7 +5,7 @@ import 'package:appflowy/plugins/shared/share/share_menu.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ShareMenuButton extends StatelessWidget {
|
||||
@ -43,27 +41,12 @@ class ShareMenuButton extends StatelessWidget {
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
child: const _ShareButton(),
|
||||
child: PrimaryRoundedButton(
|
||||
text: LocaleKeys.shareAction_buttonText.tr(),
|
||||
figmaLineHeight: 16,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareButton extends StatelessWidget {
|
||||
const _ShareButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14.0),
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10.0),
|
||||
),
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +161,10 @@ class _ExportButton extends StatelessWidget {
|
||||
borderRadius: radius,
|
||||
),
|
||||
radius: radius,
|
||||
text: FlowyText(title),
|
||||
text: FlowyText(
|
||||
title,
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
leftIcon: FlowySvg(svg),
|
||||
onTap: onTap,
|
||||
);
|
||||
|
@ -173,6 +173,7 @@ class _PublishedWidgetState extends State<_PublishedWidget> {
|
||||
),
|
||||
radius: BorderRadius.circular(10),
|
||||
text: FlowyText.regular(
|
||||
lineHeight: 1.0,
|
||||
LocaleKeys.shareAction_unPublish.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
@ -189,6 +190,7 @@ class _PublishedWidgetState extends State<_PublishedWidget> {
|
||||
title: LocaleKeys.shareAction_visitSite.tr(),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
hoverColor: Theme.of(context).colorScheme.primary.withOpacity(0.9),
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
);
|
||||
}
|
||||
@ -257,13 +259,13 @@ class _PublishButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoundedTextButton(
|
||||
height: 36,
|
||||
title: LocaleKeys.shareAction_publish.tr(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 9.0),
|
||||
return PrimaryRoundedButton(
|
||||
text: LocaleKeys.shareAction_publish.tr(),
|
||||
useIntrinsicWidth: false,
|
||||
margin: const EdgeInsets.symmetric(vertical: 9.0),
|
||||
fontSize: 14.0,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: onPublish,
|
||||
figmaLineHeight: 18.0,
|
||||
onTap: onPublish,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -101,7 +101,10 @@ class _TrashPageState extends State<TrashPage> {
|
||||
const Spacer(),
|
||||
IntrinsicWidth(
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.trash_restoreAll.tr()),
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.trash_restoreAll.tr(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.restore_s),
|
||||
onTap: () {
|
||||
NavigatorAlertDialog(
|
||||
@ -118,7 +121,10 @@ class _TrashPageState extends State<TrashPage> {
|
||||
const HSpace(6),
|
||||
IntrinsicWidth(
|
||||
child: FlowyButton(
|
||||
text: FlowyText.medium(LocaleKeys.trash_deleteAll.tr()),
|
||||
text: FlowyText.medium(
|
||||
LocaleKeys.trash_deleteAll.tr(),
|
||||
lineHeight: 1.0,
|
||||
),
|
||||
leftIcon: const FlowySvg(FlowySvgs.delete_s),
|
||||
onTap: () {
|
||||
NavigatorAlertDialog(
|
||||
|
25
frontend/appflowy_flutter/lib/shared/red_dot.dart
Normal file
25
frontend/appflowy_flutter/lib/shared/red_dot.dart
Normal file
@ -0,0 +1,25 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class NotificationRedDot extends StatelessWidget {
|
||||
const NotificationRedDot({
|
||||
super.key,
|
||||
this.size = 6,
|
||||
});
|
||||
|
||||
final double size;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: size,
|
||||
height: size,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
color: const Color(0xFFFF2214),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -4,7 +4,6 @@ import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart';
|
||||
import 'package:appflowy/plugins/trash/application/prelude.dart';
|
||||
import 'package:appflowy/shared/appflowy_cache_manager.dart';
|
||||
import 'package:appflowy/shared/custom_image_cache_manager.dart';
|
||||
@ -43,7 +42,6 @@ import 'package:flowy_infra/file_picker/file_picker_impl.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
class DependencyResolver {
|
||||
static Future<void> resolve(
|
||||
@ -100,23 +98,6 @@ void _resolveCommonService(
|
||||
},
|
||||
);
|
||||
|
||||
getIt.registerFactoryAsync<StabilityAIRepository>(
|
||||
() async {
|
||||
final result = await UserBackendService.getCurrentUserProfile();
|
||||
return result.fold(
|
||||
(s) {
|
||||
return HttpStabilityAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: s.stabilityAiKey,
|
||||
);
|
||||
},
|
||||
(e) {
|
||||
throw Exception('Failed to get user profile: ${e.msg}');
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
getIt.registerFactory<ClipboardService>(
|
||||
() => ClipboardService(),
|
||||
);
|
||||
|
@ -31,6 +31,7 @@ import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/m
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/time/duration.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:sheet/route.dart';
|
||||
@ -558,10 +559,25 @@ GoRoute _mobileCardDetailScreenRoute() {
|
||||
parentNavigatorKey: AppGlobals.rootNavKey,
|
||||
path: MobileRowDetailPage.routeName,
|
||||
pageBuilder: (context, state) {
|
||||
final args = state.extra as Map<String, dynamic>;
|
||||
var extra = state.extra as Map<String, dynamic>?;
|
||||
|
||||
if (kDebugMode && extra == null) {
|
||||
extra = _dynamicValues;
|
||||
}
|
||||
|
||||
if (extra == null) {
|
||||
return const MaterialExtendedPage(
|
||||
child: SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
|
||||
final databaseController =
|
||||
args[MobileRowDetailPage.argDatabaseController];
|
||||
final rowId = args[MobileRowDetailPage.argRowId]!;
|
||||
extra[MobileRowDetailPage.argDatabaseController];
|
||||
final rowId = extra[MobileRowDetailPage.argRowId]!;
|
||||
|
||||
if (kDebugMode) {
|
||||
_dynamicValues = extra;
|
||||
}
|
||||
|
||||
return MaterialExtendedPage(
|
||||
child: MobileRowDetailPage(
|
||||
@ -629,3 +645,8 @@ Widget _buildFadeTransition(
|
||||
Duration _slowDuration = Duration(
|
||||
milliseconds: RouteDurations.slow.inMilliseconds.round(),
|
||||
);
|
||||
|
||||
// ONLY USE IN DEBUG MODE
|
||||
// this is a workaround for the issue of GoRouter not supporting extra with complex types
|
||||
// https://github.com/flutter/flutter/issues/137248
|
||||
Map<String, dynamic> _dynamicValues = {};
|
||||
|
@ -2,18 +2,20 @@ import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
part 'download_model_bloc.freezed.dart';
|
||||
|
||||
class DownloadModelBloc extends Bloc<DownloadModelEvent, DownloadModelState> {
|
||||
DownloadModelBloc(LLMModelPB model)
|
||||
: super(DownloadModelState(model: model)) {
|
||||
: super(DownloadModelState.initial(model)) {
|
||||
on<DownloadModelEvent>(_handleEvent);
|
||||
}
|
||||
|
||||
@ -99,8 +101,21 @@ class DownloadModelState with _$DownloadModelState {
|
||||
@Default("") String object,
|
||||
@Default(0) double percent,
|
||||
@Default(false) bool isFinish,
|
||||
String? bigFileDownloadPrompt,
|
||||
@Default(ChatLoadingState.loading()) ChatLoadingState loadingState,
|
||||
}) = _DownloadModelState;
|
||||
|
||||
factory DownloadModelState.initial(LLMModelPB model) {
|
||||
// bigger than 1 GB then show download big file prompt
|
||||
String? bigFileDownloadPrompt;
|
||||
if (model.fileSize > 1 * 1024 * 1024 * 1024) {
|
||||
bigFileDownloadPrompt = LocaleKeys.settings_aiPage_keys_downloadBigFilePrompt.tr();
|
||||
}
|
||||
return DownloadModelState(
|
||||
model: model,
|
||||
bigFileDownloadPrompt: bigFileDownloadPrompt,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadingStream {
|
||||
|
@ -1,14 +1,12 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'tabs_bloc.freezed.dart';
|
||||
@ -92,8 +90,5 @@ class TabsBloc extends Bloc<TabsEvent, TabsState> {
|
||||
view: view,
|
||||
),
|
||||
);
|
||||
|
||||
// Update recent views
|
||||
getIt<CachedRecentService>().updateRecentViews([view.id], true);
|
||||
}
|
||||
}
|
||||
|
@ -10,19 +10,19 @@ class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
|
||||
ViewTitleBarBloc({
|
||||
required this.view,
|
||||
}) : super(ViewTitleBarState.initial()) {
|
||||
viewListener = ViewListener(
|
||||
viewId: view.id,
|
||||
)..start(
|
||||
onViewChildViewsUpdated: (p0) {
|
||||
add(const ViewTitleBarEvent.reload());
|
||||
},
|
||||
);
|
||||
|
||||
on<ViewTitleBarEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
add(const ViewTitleBarEvent.reload());
|
||||
|
||||
viewListener = ViewListener(
|
||||
viewId: view.id,
|
||||
)..start(
|
||||
onViewUpdated: (p0) {
|
||||
add(const ViewTitleBarEvent.reload());
|
||||
},
|
||||
);
|
||||
},
|
||||
reload: () async {
|
||||
final List<ViewPB> ancestors =
|
||||
|
@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
@ -455,12 +456,28 @@ class _SidebarSearchButton extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
onTap: () => CommandPalette.of(context).toggle(),
|
||||
leftIcon: const FlowySvg(FlowySvgs.search_s),
|
||||
iconPadding: 12.0,
|
||||
margin: const EdgeInsets.only(left: 8.0),
|
||||
text: FlowyText.regular(LocaleKeys.search_label.tr()),
|
||||
return FlowyTooltip(
|
||||
richMessage: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.search_sidebarSearchIcon.tr()}\n',
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
TextSpan(
|
||||
text: Platform.isMacOS ? '⌘+P' : 'Ctrl+P',
|
||||
style: context
|
||||
.tooltipTextStyle()
|
||||
?.copyWith(color: Theme.of(context).hintColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: FlowyButton(
|
||||
onTap: () => CommandPalette.of(context).toggle(),
|
||||
leftIcon: const FlowySvg(FlowySvgs.search_s),
|
||||
iconPadding: 12.0,
|
||||
margin: const EdgeInsets.only(left: 8.0),
|
||||
text: FlowyText.regular(LocaleKeys.search_label.tr()),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user