Merge branch 'AppFlowy-IO:main' into feat/kanban-clickable-links

This commit is contained in:
Sean Siders 2024-08-13 10:42:43 -07:00 committed by GitHub
commit ed8d4c17fe
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
231 changed files with 2993 additions and 2191 deletions

View File

@ -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.

View File

View 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();

View File

@ -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

View File

@ -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,
),

View File

@ -87,6 +87,7 @@ class _PropertyCellState extends State<_PropertyCell> {
fieldInfo.name,
overflow: TextOverflow.ellipsis,
fontSize: 14,
figmaLineHeight: 16.0,
color: Theme.of(context).hintColor,
),
),

View File

@ -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,
),

View File

@ -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

View File

@ -29,6 +29,7 @@ class FontSetting extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
lineHeight: 1.0,
name,
color: theme.colorScheme.onSurface,
),

View File

@ -38,6 +38,7 @@ class _LanguageSettingGroupState extends State<LanguageSettingGroup> {
mainAxisSize: MainAxisSize.min,
children: [
FlowyText(
lineHeight: 1.0,
languageFromLocale(locale),
color: theme.colorScheme.onSurface,
),

View File

@ -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) {

View File

@ -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,

View File

@ -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 {

View File

@ -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));
},

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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);
}

View File

@ -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),

View File

@ -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(

View File

@ -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),

View File

@ -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,

View File

@ -124,6 +124,7 @@ class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
editorScrollController: scrollController,
blockComponentBuilders: blockBuilders,
commandShortcutEvents: [customCopyCommand],
disableAutoScroll: true,
editorState: editorState,
),
);

View File

@ -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);
},
),
),
)

View File

@ -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: () {

View File

@ -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,

View File

@ -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,
);
},
),
);
}
}

View File

@ -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: () {

View File

@ -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,
),

View File

@ -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,

View File

@ -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,

View File

@ -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();

View File

@ -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,
),

View File

@ -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,

View File

@ -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,
),

View File

@ -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,
),

View File

@ -23,6 +23,7 @@ class GridAddRowButton extends StatelessWidget {
),
),
text: FlowyText(
lineHeight: 1.0,
LocaleKeys.grid_row_newRow.tr(),
color: Theme.of(context).hintColor,
),

View File

@ -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,

View File

@ -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,
),

View File

@ -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),

View File

@ -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,
};
}

View File

@ -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,

View File

@ -34,6 +34,7 @@ class SortChoiceButton extends StatelessWidget {
useIntrinsicWidth: true,
text: FlowyText(
text,
lineHeight: 1.0,
color: AFThemeExtension.of(context).textColor,
overflow: TextOverflow.ellipsis,
),

View File

@ -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;
}
}

View File

@ -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(),
};

View File

@ -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(

View File

@ -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,

View File

@ -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),
),

View File

@ -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,
),

View File

@ -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,
),

View File

@ -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: () {

View File

@ -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,
),

View File

@ -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),
),

View File

@ -193,6 +193,7 @@ class _GridGroupCell extends StatelessWidget {
text: FlowyText.medium(
name,
color: AFThemeExtension.of(context).textColor,
lineHeight: 1.0,
),
leftIcon: icon != null
? FlowySvg(

View File

@ -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);

View File

@ -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),

View File

@ -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,
),

View File

@ -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,
),

View File

@ -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(

View File

@ -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,
),

View File

@ -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(

View File

@ -154,6 +154,9 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
// callout block
insertNewLineInCalloutBlock,
// quote block
insertNewLineInQuoteBlock,
// toggle list
formatGreaterToToggleList,
insertChildNodeInsideToggleList,

View File

@ -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(

View File

@ -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,
),

View File

@ -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,
);
}

View File

@ -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';

View File

@ -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';

View File

@ -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(),
},
);
}
}
}

View File

@ -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 {

View File

@ -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;

View File

@ -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';

View File

@ -84,7 +84,7 @@ class _DatabaseBlockComponentWidgetState
child: FocusScope(
skipTraversal: true,
onFocusChange: (value) {
if (value) {
if (value && keepEditorFocusNotifier.value == 0) {
context.read<EditorState>().selection = null;
}
},

View File

@ -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,
),
),
];

View File

@ -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),
],
),
);

View File

@ -247,6 +247,7 @@ class _CoverImagePreviewWidgetState extends State<CoverImagePreviewWidget> {
size: Size(20, 20),
),
text: FlowyText(
lineHeight: 1.0,
LocaleKeys.document_plugins_cover_pickFromFiles.tr(),
),
),

View File

@ -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,
),
),
];

View File

@ -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,

View File

@ -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();

View File

@ -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

View File

@ -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),
],
);
}

View File

@ -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,
);
});
}
}

View File

@ -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,
);
});
}
}

View File

@ -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,),
],
),
);

View File

@ -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');

View File

@ -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';

View File

@ -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;
};

View File

@ -448,7 +448,7 @@ Widget _slashMenuItemNameBuilder(
SelectionMenuStyle style,
bool isSelected,
) {
return FlowyText(
return FlowyText.regular(
name,
fontSize: 12.0,
figmaLineHeight: 15.0,

View File

@ -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(),
),
);
}
}
}

View File

@ -1,10 +0,0 @@
class StabilityAIRequestError {
StabilityAIRequestError(this.message);
final String message;
@override
String toString() {
return 'StabilityAIRequestError{message: $message}';
}
}

View File

@ -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,
);
}
}

View File

@ -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,
);

View File

@ -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,
);
}
}

View File

@ -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(

View 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),
),
),
);
}
}

View File

@ -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(),
);

View File

@ -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 = {};

View File

@ -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 {

View File

@ -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);
}
}

View File

@ -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 =

View File

@ -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