Compare commits

..
16 Commits
Author SHA1 Message Date
Lucas.Xu f36e3ae378 chore: bump version 0.6.4 (#5744) 2024-07-16 14:51:00 +08:00
Mathias MogensenandLucas.Xu dc6349b4b1 fix: improve color selector (#5743)
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-16 14:50:50 +08:00
Lucas.Xu 44d8def3ce fix: reset space relationship when clearing cache (#5737)
* fix: space's child views didn't update when moving a page into it

* chore: remove check logic

* feat: integrate fix space into clear cache

* fix: translation missing value

* chore: update logger version
2024-07-16 13:23:07 +08:00
Nathan.fooo c6ad57f11d chore: show plugin state (#5740)
* chore: show plugin state

* chore: flutter analyzer

* chore: update
2024-07-15 22:45:53 +08:00
Nathan.fooo ff23165d3e chore: download llm files (#5723)
* chore: download file

* chore: config download ui

* chore: update zip

* chore: config download ui

* chore: unzip file

* chore: unzip file

* chore: rename

* chore: disable local ai

* chore: fmt

* chore: fix warning

* chore: update

* chore: fix clippy
2024-07-15 15:23:23 +08:00
Lucas.Xu 253e7597c4 feat: optimize the read recent views speed (#5726)
* feat: optimize the read recent views speed

* fix: order of recent views should be from the latest to the oldest
2024-07-15 14:23:20 +08:00
Zack d2e3fdfefd chore: fix compile (#5733) 2024-07-15 09:01:34 +08:00
Lucas.Xu a2336f5bf7 chore: improve popup menu color (#5725)
* chore: improve popup menu color

* fix: 100% text wrap into a new line in checklist cell
2024-07-12 15:23:22 +08:00
Lucas.Xu 8833df1740 fix: ai chat result contains html escape text (#5721)
* fix: ai chat result contains html escape text

* feat: support copy the code block from ai chat page

* feat: support copy code block from ai chat page

* fix: add hover effect to color picker button

* chore: increase minimum window size
2024-07-11 21:42:02 +08:00
Lucas.Xu 1524b10a8a chore: bump version 0.6.4 (#5719)
* chore: upgrade android target sdk to 34

* chore: downgrade macos builder version to support x86_64

* chore: bump version 0.6.4
2024-07-11 13:46:28 +08:00
Kilu.He fe0fa9b530 feat: support sign-in and sign-up on Web (#5712) 2024-07-11 12:55:22 +08:00
Lucas.Xu 80afcf44c0 fix: syntax error when generating translation files (#5718) 2024-07-11 11:56:00 +08:00
e500c89978 feat: choose cursor/selection color from palette or color picker #5041 (#5677)
* chore: show ai service error (#5675)

* feat: Implement color picker dialog for user color selection (#5041)

- Added `flex_color_picker` dependency.
- Implemented a new color picker dialog with the following features:
  - Display Material predefined colors.
  - Include primary, accent, and shade colors, as well as a color wheel for selection.
  - Add a graphical opacity selector.
  - Enhanced the previous dialog to include an icon in a text field for opening the palette.
- Added `suffixIcon` parameter to the `_ColorSettingTextField` class, making it reactive to tap gestures.
- Utilized `ColorExtension` on the `Color` class to avoid namespace conflicts when converting colors to hexadecimal strings.
- Added translation strings to english

This commit addresses issue #5041

* feat: Implement color picker dialog for user color selection (#5041)

- Added `flex_color_picker` dependency.
- Implemented a new color picker dialog with the following features:
      - Display Material predefined colors.
      - Include primary, accent, and shade colors, as well as a color wheel for selection.
      - Add a graphical opacity selector.
      - Enhanced the previous dialog to include an icon in a text field for opening the palette.
- Added `suffixIcon` parameter to the `_ColorSettingTextField` class, making it reactive to tap gestures.
- Utilized `ColorExtension` on the `Color` class to avoid namespace conflicts when converting colors to hexadecimal strings.
- Added translation strings to english

This commit addresses issue #5041

* chore: refactor code

---------

Co-authored-by: Nathan.fooo <86001920+appflowy@users.noreply.github.com>
Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
2024-07-10 17:49:59 +08:00
Lucas.Xu 079b9888a8 feat: render ai text message with appflowy_editor (#5682)
* feat: render ai text message with appflowy_editor

* chore: update appflowy_editor

* fix: integration test

* feat: support paste inAppJson format

* chore: update appflowy_editor
2024-07-10 17:17:07 +08:00
Lucas.Xu 0fe383e538 feat: enable debug logs in internal build (#5713) 2024-07-10 13:55:40 +08:00
Bartosz Sypytkowski c006e29afc chore: update yrs and appflowy collab dependencies (#5707)
* chore: update yrs and appflowy collab dependencies

* chore: fix clippy errors

* chore: update yrs and collab dependencies in appflowy tauri

* chore: update yrs and collab dependencies in appflowy web app
2024-07-10 10:10:21 +08:00
164 changed files with 6145 additions and 1673 deletions

No files matched your search

+1 -1
View File
@@ -135,7 +135,7 @@ jobs:
fail-fast: false
matrix:
job:
- { target: x86_64-apple-darwin, os: macos-latest, extra-build-args: "" }
- { target: x86_64-apple-darwin, os: macos-12, extra-build-args: "" }
steps:
- name: Checkout source code
uses: actions/checkout@v4
+8
View File
@@ -1,4 +1,12 @@
# Release Notes
## Version 0.6.4 - 16/07/2024
### New Features
- Enhanced the message style on the AI chat page.
- Added the ability to choose cursor color and selection color from a palette in settings page.
### Bug Fixes
- Optimized the performance for loading recent pages.
- Fixed an issue where the cursor would jump randomly when typing in the document title on mobile.
## Version 0.6.3 - 08/07/2024
### New Features
- Publish a Document to the Web
+1 -1
View File
@@ -138,4 +138,4 @@
"cwd": "${workspaceRoot}/appflowy_tauri/"
},
]
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.3"
APPFLOWY_VERSION = "0.6.4"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
@@ -53,7 +53,7 @@ android {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "io.appflowy.appflowy"
minSdkVersion 24
targetSdkVersion 33
targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
@@ -82,11 +82,11 @@ void main() {
HeadingBlockKeys.type,
);
expect(
importedPageEditorState.getNodeAtPath([2])!.type,
importedPageEditorState.getNodeAtPath([1])!.type,
HeadingBlockKeys.type,
);
expect(
importedPageEditorState.getNodeAtPath([4])!.type,
importedPageEditorState.getNodeAtPath([2])!.type,
TableBlockKeys.type,
);
});
@@ -13,7 +13,9 @@ import 'util.dart';
extension AppFlowyAuthTest on WidgetTester {
Future<void> tapGoogleLoginInButton() async {
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
await tapButton(
find.byKey(const Key('signInWithGoogleButton')),
);
}
/// Requires being on the SettingsPage.account of the SettingsDialog
+8 -2
View File
@@ -65,6 +65,8 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- printing (1.0.0):
- Flutter
- ReachabilitySwift (5.0.0)
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
@@ -101,6 +103,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- printing (from `.symlinks/plugins/printing/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@@ -149,6 +152,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
printing:
:path: ".symlinks/plugins/printing/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -170,7 +175,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@@ -179,6 +184,7 @@ SPEC CHECKSUMS:
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
@@ -191,4 +197,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.11.3
COCOAPODS: 1.15.2
+7
View File
@@ -29,4 +29,11 @@ abstract class Env {
defaultValue: '',
)
static const String afCloudUrl = _Env.afCloudUrl;
@EnviedField(
obfuscate: false,
varName: 'INTERNAL_BUILD',
defaultValue: '',
)
static const String internalBuild = _Env.internalBuild;
}
@@ -38,8 +38,7 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
builder: (context, state) {
final ids = <String>{};
List<ViewPB> recentViews =
state.views.reversed.map((e) => e.item).toList();
List<ViewPB> recentViews = state.views.map((e) => e.item).toList();
recentViews.retainWhere((element) => ids.add(element.id));
// only keep the first 20 items.
@@ -49,7 +49,7 @@ class _MobileRecentSpaceState extends State<MobileRecentSpace>
List<SectionViewPB> _filterRecentViews(List<SectionViewPB> recentViews) {
final ids = <String>{};
final filteredRecentViews = recentViews.reversed.toList();
final filteredRecentViews = recentViews.toList();
filteredRecentViews.retainWhere((e) => ids.add(e.item.id));
return filteredRecentViews;
}
@@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget {
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
onTap: () => afLaunchUrlString('https://appflowy.io/terms'),
),
if (kDebugMode)
MobileSettingItem(
@@ -7,6 +7,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
@@ -480,7 +481,7 @@ class ChatState with _$ChatState {
@freezed
class LoadingState with _$LoadingState {
const factory LoadingState.loading() = _Loading;
const factory LoadingState.finish() = _Finish;
const factory LoadingState.finish({FlowyError? error}) = _Finish;
}
enum OnetimeShotType {
@@ -0,0 +1,83 @@
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'chat_file_bloc.freezed.dart';
class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
ChatFileBloc({
required String chatId,
dynamic message,
}) : listener = LocalLLMListener(),
super(ChatFileState.initial(message)) {
listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(ChatFileEvent.updateLocalAIState(pluginState));
}
},
);
on<ChatFileEvent>(
(event, emit) async {
await event.when(
initial: () async {
final result = await ChatEventGetPluginState().send();
result.fold(
(pluginState) {
if (!isClosed) {
add(ChatFileEvent.updateLocalAIState(pluginState));
}
},
(err) {},
);
},
newFile: (String filePath) {
final payload = ChatFilePB(filePath: filePath, chatId: chatId);
ChatEventChatWithFile(payload).send();
},
updateLocalAIState: (PluginStatePB pluginState) {
emit(
state.copyWith(
supportChatWithFile:
pluginState.state == RunningStatePB.Running,
),
);
},
);
},
);
}
final LocalLLMListener listener;
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
}
@freezed
class ChatFileEvent with _$ChatFileEvent {
const factory ChatFileEvent.initial() = Initial;
const factory ChatFileEvent.newFile(String filePath) = _NewFile;
const factory ChatFileEvent.updateLocalAIState(PluginStatePB pluginState) =
_UpdateLocalAIState;
}
@freezed
class ChatFileState with _$ChatFileState {
const factory ChatFileState({
required String text,
@Default(false) bool supportChatWithFile,
}) = _ChatFileState;
factory ChatFileState.initial(dynamic text) {
return ChatFileState(
text: text is String ? text : "",
);
}
}
@@ -1,3 +1,5 @@
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
@@ -50,7 +52,7 @@ class AIChatUILayout {
}
}
class AIChatPage extends StatefulWidget {
class AIChatPage extends StatelessWidget {
const AIChatPage({
super.key,
required this.view,
@@ -63,10 +65,58 @@ class AIChatPage extends StatefulWidget {
final UserProfilePB userProfile;
@override
State<AIChatPage> createState() => _AIChatPageState();
Widget build(BuildContext context) {
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
return BlocProvider(
create: (context) => ChatFileBloc(chatId: view.id.toString()),
child: BlocBuilder<ChatFileBloc, ChatFileState>(
builder: (context, state) {
return state.supportChatWithFile
? DropTarget(
onDragDone: (DropDoneDetails detail) async {
for (final file in detail.files) {
context
.read<ChatFileBloc>()
.add(ChatFileEvent.newFile(file.path));
}
},
child: _ChatContentPage(
view: view,
userProfile: userProfile,
),
)
: _ChatContentPage(
view: view,
userProfile: userProfile,
);
},
),
);
}
return Center(
child: FlowyText(
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
fontSize: 20,
),
);
}
}
class _AIChatPageState extends State<AIChatPage> {
class _ChatContentPage extends StatefulWidget {
const _ChatContentPage({
required this.view,
required this.userProfile,
});
final UserProfilePB userProfile;
final ViewPB view;
@override
State<_ChatContentPage> createState() => _ChatContentPageState();
}
class _ChatContentPageState extends State<_ChatContentPage> {
late types.User _user;
@override
@@ -1,10 +1,16 @@
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
@@ -176,9 +182,20 @@ class CopyButton extends StatelessWidget {
size: const Size.square(14),
color: Theme.of(context).colorScheme.primary,
),
onPressed: () {
Clipboard.setData(ClipboardData(text: textMessage.text));
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
onPressed: () async {
final document = customMarkdownToDocument(textMessage.text);
await getIt<ClipboardService>().setData(
ClipboardServiceData(
plainText: textMessage.text,
inAppJson: jsonEncode(document.toJson()),
),
);
if (context.mounted) {
showToastNotification(
context,
message: LocaleKeys.grid_url_copiedNotification.tr(),
);
}
},
),
);
@@ -1,10 +1,9 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'chat_input.dart';
@@ -0,0 +1,373 @@
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart';
enum AIMarkdownType {
appflowyEditor,
markdownWidget,
}
// Wrap the appflowy_editor or markdown_widget as a chat text message widget
class AIMarkdownText extends StatelessWidget {
const AIMarkdownText({
super.key,
required this.markdown,
this.type = AIMarkdownType.appflowyEditor,
});
final String markdown;
final AIMarkdownType type;
@override
Widget build(BuildContext context) {
switch (type) {
case AIMarkdownType.appflowyEditor:
return _AppFlowyEditorMarkdown(markdown: markdown);
case AIMarkdownType.markdownWidget:
return _ThirdPartyMarkdown(markdown: markdown);
}
}
}
class _AppFlowyEditorMarkdown extends StatefulWidget {
const _AppFlowyEditorMarkdown({
required this.markdown,
});
// the text should be the markdown format
final String markdown;
@override
State<_AppFlowyEditorMarkdown> createState() =>
_AppFlowyEditorMarkdownState();
}
class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
late EditorState editorState;
late final styleCustomizer = EditorStyleCustomizer(
context: context,
padding: EdgeInsets.zero,
);
late final editorStyle = styleCustomizer.style().copyWith(
// hide the cursor
cursorColor: Colors.transparent,
cursorWidth: 0,
);
late EditorScrollController scrollController;
@override
void initState() {
super.initState();
editorState = _parseMarkdown(widget.markdown);
scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
}
@override
void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.markdown != widget.markdown) {
editorState.dispose();
editorState = _parseMarkdown(widget.markdown);
scrollController.dispose();
scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
}
}
@override
void dispose() {
scrollController.dispose();
editorState.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final blockBuilders = getEditorBuilderMap(
context: context,
editorState: editorState,
styleCustomizer: styleCustomizer,
// the editor is not editable in the chat
editable: false,
);
return IntrinsicHeight(
child: AppFlowyEditor(
shrinkWrap: true,
// the editor is not editable in the chat
editable: false,
editorStyle: editorStyle,
editorScrollController: scrollController,
blockComponentBuilders: blockBuilders,
commandShortcutEvents: [customCopyCommand],
editorState: editorState,
),
);
}
EditorState _parseMarkdown(String markdown) {
final document = customMarkdownToDocument(markdown);
final editorState = EditorState(document: document);
return editorState;
}
}
class _ThirdPartyMarkdown extends StatelessWidget {
const _ThirdPartyMarkdown({
required this.markdown,
});
final String markdown;
@override
Widget build(BuildContext context) {
return MarkdownWidget(
data: markdown,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
_ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
_ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
_ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
builder: (code, language) {
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 800,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
child: SelectableHighlightView(
code,
language: language,
theme: getHighlightTheme(context),
padding: const EdgeInsets.all(14),
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
),
);
},
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig(
sideColor: AFThemeExtension.of(context).lightGreyHover,
textColor: AFThemeExtension.of(context).textColor,
),
],
);
}
Map<String, TextStyle> getHighlightTheme(BuildContext context) {
return {
'root': TextStyle(
color: const Color(0xffabb2bf),
backgroundColor:
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
),
'comment': const TextStyle(
color: Color(0xff5c6370),
fontStyle: FontStyle.italic,
),
'quote': const TextStyle(
color: Color(0xff5c6370),
fontStyle: FontStyle.italic,
),
'doctag': const TextStyle(color: Color(0xffc678dd)),
'keyword': const TextStyle(color: Color(0xffc678dd)),
'formula': const TextStyle(color: Color(0xffc678dd)),
'section': const TextStyle(color: Color(0xffe06c75)),
'name': const TextStyle(color: Color(0xffe06c75)),
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
'deletion': const TextStyle(color: Color(0xffe06c75)),
'subst': const TextStyle(color: Color(0xffe06c75)),
'literal': const TextStyle(color: Color(0xff56b6c2)),
'string': const TextStyle(color: Color(0xff98c379)),
'regexp': const TextStyle(color: Color(0xff98c379)),
'addition': const TextStyle(color: Color(0xff98c379)),
'attribute': const TextStyle(color: Color(0xff98c379)),
'meta-string': const TextStyle(color: Color(0xff98c379)),
'built_in': const TextStyle(color: Color(0xffe6c07b)),
'attr': const TextStyle(color: Color(0xffd19a66)),
'variable': const TextStyle(color: Color(0xffd19a66)),
'template-variable': const TextStyle(color: Color(0xffd19a66)),
'type': const TextStyle(color: Color(0xffd19a66)),
'selector-class': const TextStyle(color: Color(0xffd19a66)),
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
'number': const TextStyle(color: Color(0xffd19a66)),
'symbol': const TextStyle(color: Color(0xff61aeee)),
'bullet': const TextStyle(color: Color(0xff61aeee)),
'link': const TextStyle(color: Color(0xff61aeee)),
'meta': const TextStyle(color: Color(0xff61aeee)),
'selector-id': const TextStyle(color: Color(0xff61aeee)),
'title': const TextStyle(color: Color(0xff61aeee)),
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
'strong': const TextStyle(fontWeight: FontWeight.bold),
};
}
}
class _ChatH1Config extends HeadingConfig {
const _ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class _ChatH2Config extends HeadingConfig {
const _ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class _ChatH3Config extends HeadingConfig {
const _ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
@@ -2,19 +2,15 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart';
class ChatAITextMessageWidget extends StatelessWidget {
const ChatAITextMessageWidget({
@@ -59,248 +55,12 @@ class ChatAITextMessageWidget extends StatelessWidget {
if (state.text.isEmpty) {
return const ChatAILoading();
} else {
return _textWidgetBuilder(user, context, state.text);
return AIMarkdownText(markdown: state.text);
}
},
),
);
}
Widget _textWidgetBuilder(
User user,
BuildContext context,
String text,
) {
return MarkdownWidget(
data: text,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
builder: (code, language) {
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 800,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
child: SelectableHighlightView(
code,
language: language,
theme: getHightlineTheme(context),
padding: const EdgeInsets.all(14),
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
),
);
},
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig(
sideColor: AFThemeExtension.of(context).lightGreyHover,
textColor: AFThemeExtension.of(context).textColor,
),
],
);
}
}
Map<String, TextStyle> getHightlineTheme(BuildContext context) {
return {
'root': TextStyle(
color: const Color(0xffabb2bf),
backgroundColor:
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
),
'comment':
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
'quote':
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
'doctag': const TextStyle(color: Color(0xffc678dd)),
'keyword': const TextStyle(color: Color(0xffc678dd)),
'formula': const TextStyle(color: Color(0xffc678dd)),
'section': const TextStyle(color: Color(0xffe06c75)),
'name': const TextStyle(color: Color(0xffe06c75)),
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
'deletion': const TextStyle(color: Color(0xffe06c75)),
'subst': const TextStyle(color: Color(0xffe06c75)),
'literal': const TextStyle(color: Color(0xff56b6c2)),
'string': const TextStyle(color: Color(0xff98c379)),
'regexp': const TextStyle(color: Color(0xff98c379)),
'addition': const TextStyle(color: Color(0xff98c379)),
'attribute': const TextStyle(color: Color(0xff98c379)),
'meta-string': const TextStyle(color: Color(0xff98c379)),
'built_in': const TextStyle(color: Color(0xffe6c07b)),
'attr': const TextStyle(color: Color(0xffd19a66)),
'variable': const TextStyle(color: Color(0xffd19a66)),
'template-variable': const TextStyle(color: Color(0xffd19a66)),
'type': const TextStyle(color: Color(0xffd19a66)),
'selector-class': const TextStyle(color: Color(0xffd19a66)),
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
'number': const TextStyle(color: Color(0xffd19a66)),
'symbol': const TextStyle(color: Color(0xff61aeee)),
'bullet': const TextStyle(color: Color(0xff61aeee)),
'link': const TextStyle(color: Color(0xff61aeee)),
'meta': const TextStyle(color: Color(0xff61aeee)),
'selector-id': const TextStyle(color: Color(0xff61aeee)),
'title': const TextStyle(color: Color(0xff61aeee)),
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
'strong': const TextStyle(fontWeight: FontWeight.bold),
};
}
class ChatH1Config extends HeadingConfig {
const ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class ChatH2Config extends HeadingConfig {
const ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class ChatH3Config extends HeadingConfig {
const ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class StreamingError extends StatelessWidget {
@@ -52,7 +52,6 @@ class TextMessageText extends StatelessWidget {
text,
fontSize: 16,
fontWeight: FontWeight.w500,
lineHeight: 1.5,
maxLines: null,
selectable: true,
color: AFThemeExtension.of(context).textColor,
@@ -52,7 +52,7 @@ class DatabaseTabBarBloc
_createLinkedView(layout.layoutType, name ?? layout.layoutName);
},
deleteView: (String viewId) async {
final result = await ViewBackendService.delete(viewId: viewId);
final result = await ViewBackendService.deleteView(viewId: viewId);
result.fold(
(l) {},
(r) => Log.error(r),
@@ -1,6 +1,6 @@
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -1,7 +1,7 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:percent_indicator/percent_indicator.dart';
import '../../application/cell/bloc/checklist_cell_bloc.dart';
class ChecklistProgressBar extends StatefulWidget {
@@ -68,7 +68,7 @@ class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
),
),
SizedBox(
width: PlatformExtension.isDesktop ? 36 : 45,
width: 45,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Text(
@@ -18,6 +18,7 @@ import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/util/debounce.dart';
import 'package:appflowy/util/throttle.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -37,6 +38,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_bloc.freezed.dart';
bool enableDocumentInternalLog = false;
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
DocumentBloc({
required this.documentId,
@@ -212,6 +215,10 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
}
Future<EditorState?> _initAppFlowyEditorState(DocumentDataPB data) async {
if (enableDocumentInternalLog) {
Log.info('document data: ${data.toProto3Json()}');
}
final document = data.toDocument();
if (document == null) {
assert(false, 'document is null');
@@ -180,6 +180,8 @@ extension NodeToBlock on Node {
String? parentId,
String? childrenId,
Attributes? attributes,
String? externalId,
String? externalType,
}) {
assert(id.isNotEmpty);
final block = BlockPB.create()
@@ -192,6 +194,12 @@ extension NodeToBlock on Node {
if (parentId != null && parentId.isNotEmpty) {
block.parentId = parentId;
}
if (externalId != null && externalId.isNotEmpty) {
block.externalId = externalId;
}
if (externalType != null && externalType.isNotEmpty) {
block.externalType = externalType;
}
return block;
}
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy_backend/log.dart';
@@ -21,6 +22,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
import 'package:collection/collection.dart';
import 'package:nanoid/nanoid.dart';
const _kExternalTextType = 'text';
/// Uses to adjust the data structure between the editor and the backend.
///
/// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
@@ -34,11 +37,9 @@ class TransactionAdapter {
final DocumentService documentService;
final String documentId;
final bool _enableDebug = false;
Future<void> apply(Transaction transaction, EditorState editorState) async {
final stopwatch = Stopwatch()..start();
if (_enableDebug) {
if (enableDocumentInternalLog) {
Log.debug('transaction => ${transaction.toJson()}');
}
final actions = transaction.operations
@@ -60,8 +61,10 @@ class TransactionAdapter {
textId: payload.textId,
delta: payload.delta,
);
if (_enableDebug) {
Log.debug('create external text: ${payload.delta}');
if (enableDocumentInternalLog) {
Log.debug(
'[editor_transaction_adapter] create external text: ${payload.delta}',
);
}
} else if (type == TextDeltaType.update) {
await documentService.updateExternalText(
@@ -69,8 +72,10 @@ class TransactionAdapter {
textId: payload.textId,
delta: payload.delta,
);
if (_enableDebug) {
Log.debug('update external text: ${payload.delta}');
if (enableDocumentInternalLog) {
Log.debug(
'[editor_transaction_adapter] update external text: ${payload.delta}',
);
}
}
}
@@ -82,9 +87,9 @@ class TransactionAdapter {
);
final elapsed = stopwatch.elapsedMilliseconds;
stopwatch.stop();
if (_enableDebug) {
if (enableDocumentInternalLog) {
Log.debug(
'apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
'[editor_transaction_adapter] apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
);
}
}
@@ -136,8 +141,9 @@ extension on InsertOperation {
// create the external text if the node contains the delta in its data.
final delta = node.delta;
TextDeltaPayloadPB? textDeltaPayloadPB;
String? textId;
if (delta != null) {
final textId = nanoid(6);
textId = nanoid(6);
textDeltaPayloadPB = TextDeltaPayloadPB(
documentId: documentId,
@@ -148,13 +154,17 @@ extension on InsertOperation {
// sync the text id to the node
node.externalValues = ExternalValues(
externalId: textId,
externalType: 'text',
externalType: _kExternalTextType,
);
}
// remove the delta from the data when the incremental update is stable.
final payload = BlockActionPayloadPB()
..block = node.toBlock(childrenId: nanoid(6))
..block = node.toBlock(
childrenId: nanoid(6),
externalId: textId,
externalType: textId != null ? _kExternalTextType : null,
)
..parentId = parentId
..prevId = prevId;
@@ -248,7 +258,7 @@ extension on UpdateOperation {
node.externalValues = ExternalValues(
externalId: textId,
externalType: 'text',
externalType: _kExternalTextType,
);
actions.add(
@@ -1,16 +1,17 @@
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
CodeBlockCopyBuilder codeBlockCopyBuilder =
(_, node) => _CopyButton(node: node);
@@ -28,16 +29,26 @@ class _CopyButton extends StatelessWidget {
message: LocaleKeys.document_codeBlock_copyTooltip.tr(),
child: FlowyIconButton(
onPressed: () async {
final delta = node.delta;
if (delta == null) {
return;
}
final document = Document.blank()
..insert([0], [node.copyWith()])
..toJson();
await getIt<ClipboardService>().setData(
ClipboardServiceData(
plainText: node.delta?.toPlainText(),
plainText: delta.toPlainText(),
inAppJson: jsonEncode(document.toJson()),
),
);
if (context.mounted) {
showSnackBarMessage(
showToastNotification(
context,
LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
);
}
},
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
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';
@@ -10,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
@@ -54,6 +53,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
// try to paste the content in order, if any of them is failed, then try the next one
if (inAppJson != null && inAppJson.isNotEmpty) {
debugPrint('paste in app json: $inAppJson');
await editorState.deleteSelectionIfNeeded();
if (await editorState.pasteInAppJson(inAppJson)) {
return;
@@ -75,7 +75,9 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
child: BlocConsumer<DocumentImmersiveCoverBloc,
DocumentImmersiveCoverState>(
listener: (context, state) {
textEditingController.text = state.name;
if (textEditingController.text.isEmpty) {
textEditingController.text = state.name;
}
},
builder: (_, state) {
final iconAndTitle = _buildIconAndTitle(context, state);
@@ -159,8 +161,16 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
state.cover.isNone || state.cover.isPresets ? null : Colors.white,
overflow: TextOverflow.ellipsis,
),
onChanged: _rename,
onSubmitted: _rename,
onChanged: (name) => Debounce.debounce(
'rename',
const Duration(milliseconds: 300),
() => _rename(name),
),
onSubmitted: (name) => Debounce.debounce(
'rename',
const Duration(milliseconds: 300),
() => _rename(name),
),
);
}
@@ -0,0 +1,50 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:markdown/markdown.dart' as md;
class MarkdownCodeBlockParser extends CustomMarkdownParser {
const MarkdownCodeBlockParser();
@override
List<Node> transform(
md.Node element,
List<CustomMarkdownParser> parsers, {
MarkdownListType listType = MarkdownListType.unknown,
int? startNumber,
}) {
if (element is! md.Element) {
return [];
}
if (element.tag != 'pre') {
return [];
}
final ec = element.children;
if (ec == null || ec.isEmpty) {
return [];
}
final code = ec.first;
if (code is! md.Element || code.tag != 'code') {
return [];
}
String? language;
if (code.attributes.containsKey('class')) {
final classes = code.attributes['class']!.split(' ');
final languageClass = classes.firstWhere(
(c) => c.startsWith('language-'),
orElse: () => '',
);
language = languageClass.substring('language-'.length);
}
return [
codeBlockNode(
language: language,
delta: Delta()..insert(code.textContent.trimRight()),
),
];
}
}
@@ -1,3 +1,4 @@
export 'callout_node_parser.dart';
export 'markdown_code_parser.dart';
export 'math_equation_node_parser.dart';
export 'toggle_list_node_parser.dart';
@@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/me
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/plugins/inline_actions/service_handler.dart';
import 'package:appflowy/shared/list_extension.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';
@@ -64,11 +65,9 @@ class InlinePageReferenceService extends InlineActionsDelegate {
_recentViewsInitialized = true;
final views = (await _recentService.recentViews())
.reversed
.map((e) => e.item)
.toSet()
.toList();
final sectionViews = await _recentService.recentViews();
final views =
sectionViews.unique((e) => e.item.id).map((e) => e.item).toList();
// Filter by viewLayout
views.retainWhere(
@@ -35,6 +35,9 @@ enum FeatureFlag {
// used for controlling whether to show plan+billing options in settings
planBilling,
// used for space design
spaceDesign,
// used for ignore the conflicted feature flag
unknown;
@@ -88,6 +91,8 @@ enum FeatureFlag {
bool get isOn {
if ([
// release this feature in version 0.6.1
FeatureFlag.spaceDesign,
// release this feature in version 0.5.9
FeatureFlag.search,
// release this feature in version 0.5.6
@@ -105,15 +110,16 @@ enum FeatureFlag {
}
switch (this) {
case FeatureFlag.search:
case FeatureFlag.syncDocument:
case FeatureFlag.syncDatabase:
case FeatureFlag.spaceDesign:
return true;
case FeatureFlag.collaborativeWorkspace:
case FeatureFlag.membersSettings:
case FeatureFlag.planBilling:
case FeatureFlag.unknown:
return false;
case FeatureFlag.search:
case FeatureFlag.syncDocument:
case FeatureFlag.syncDatabase:
return true;
}
}
@@ -131,6 +137,8 @@ enum FeatureFlag {
return 'if it\'s on, the command palette and search button will be available';
case FeatureFlag.planBilling:
return 'if it\'s on, plan and billing pages will be available in Settings';
case FeatureFlag.spaceDesign:
return 'if it\'s on, the space design feature will be available';
case FeatureFlag.unknown:
return '';
}
@@ -0,0 +1,11 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
Document customMarkdownToDocument(String markdown) {
return markdownToDocument(
markdown,
markdownParsers: [
const MarkdownCodeBlockParser(),
],
);
}
@@ -6,8 +6,8 @@ import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
class WindowSizeManager {
static const double minWindowHeight = 600.0;
static const double minWindowWidth = 800.0;
static const double minWindowHeight = 640.0;
static const double minWindowWidth = 960.0;
// Preventing failed assertion due to Texture Descriptor Validation
static const double maxWindowHeight = 8192.0;
static const double maxWindowWidth = 8192.0;
@@ -54,16 +54,8 @@ class DesktopSignInScreen extends StatelessWidget {
const SignInAnonymousButtonV2(),
const VSpace(10),
SwitchSignInSignUpButton(
onTap: () {
final type = state.loginType == LoginType.signIn
? LoginType.signUp
: LoginType.signIn;
context
.read<SignInBloc>()
.add(SignInEvent.switchLoginType(type));
},
),
// sign in agreement
const SignInAgreement(),
// loading status
const VSpace(indicatorMinHeight),
@@ -39,16 +39,7 @@ class MobileSignInScreen extends StatelessWidget {
const VSpace(spacing),
const SignInAnonymousButtonV2(),
const VSpace(spacing),
SwitchSignInSignUpButton(
onTap: () {
final type = state.loginType == LoginType.signIn
? LoginType.signUp
: LoginType.signIn;
context.read<SignInBloc>().add(
SignInEvent.switchLoginType(type),
);
},
),
const SignInAgreement(),
const VSpace(spacing),
_buildSettingsButton(context),
if (!isAuthEnabled) const Spacer(flex: 2),
@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
import 'package:toastification/toastification.dart';
class SignInWithMagicLinkButtons extends StatefulWidget {
const SignInWithMagicLinkButtons({super.key});
@@ -53,18 +53,19 @@ class _SignInWithMagicLinkButtonsState
void _sendMagicLink(BuildContext context, String email) {
if (!isEmail(email)) {
return showSnackBarMessage(
return showToastNotification(
context,
LocaleKeys.signIn_invalidEmail.tr(),
duration: const Duration(seconds: 8),
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
}
context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email));
showSnackBarMessage(
context,
LocaleKeys.signIn_magicLinkSent.tr(),
duration: const Duration(seconds: 1000),
showConfirmDialog(
context: context,
title: LocaleKeys.signIn_magicLinkSent.tr(),
description: LocaleKeys.signIn_magicLinkSentDescription.tr(),
);
}
}
@@ -0,0 +1,44 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class SignInAgreement extends StatelessWidget {
const SignInAgreement({
super.key,
});
@override
Widget build(BuildContext context) {
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.web_signInAgreement.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
TextSpan(
text: '${LocaleKeys.web_termOfUse.tr()} ',
style: const TextStyle(color: Colors.blue, fontSize: 12),
mouseCursor: SystemMouseCursors.click,
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString('https://appflowy.io/terms'),
),
TextSpan(
text: '${LocaleKeys.web_and.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
TextSpan(
text: LocaleKeys.web_privacyPolicy.tr(),
style: const TextStyle(color: Colors.blue, fontSize: 12),
mouseCursor: SystemMouseCursors.click,
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString('https://appflowy.io/privacy'),
),
],
),
);
}
}
@@ -1,5 +1,7 @@
export 'magic_link_sign_in_buttons.dart';
export 'sign_in_anonymous_button.dart';
export 'sign_in_or_logout_button.dart';
export 'switch_sign_in_sign_up_button.dart';
// export 'switch_sign_in_sign_up_button.dart';
export 'third_party_sign_in_buttons.dart';
export 'sign_in_agreement.dart';
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/recent_listener.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -7,6 +8,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
/// This is a lazy-singleton to share recent views across the application.
@@ -37,7 +39,7 @@ class CachedRecentService {
_listener.start(recentViewsUpdated: _recentViewsUpdated);
_recentViews = await _readRecentViews().fold(
(s) => s.items,
(s) => s.items.unique((e) => e.item.id),
(_) => [],
);
_completer.complete();
@@ -68,7 +70,8 @@ class CachedRecentService {
Future<FlowyResult<RepeatedRecentViewPB, FlowyError>>
_readRecentViews() async {
final result = await FolderEventReadRecentViews().send();
final payload = ReadRecentViewsPB(start: Int64(), limit: Int64(100));
final result = await FolderEventReadRecentViews(payload).send();
return result.fold(
(recentViews) {
return FlowyResult.success(
@@ -101,7 +104,7 @@ class CachedRecentService {
final viewIds = result.toNullable();
if (viewIds != null) {
_recentViews = await _readRecentViews().fold(
(s) => s.items,
(s) => s.items.unique((e) => e.item.id),
(_) => [],
);
}
@@ -0,0 +1,140 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.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)) {
on<DownloadModelEvent>(_handleEvent);
}
Future<void> _handleEvent(
DownloadModelEvent event,
Emitter<DownloadModelState> emit,
) async {
await event.when(
started: () async {
final downloadStream = DownloadingStream();
downloadStream.listen(
onModelPercentage: (name, percent) {
if (!isClosed) {
add(
DownloadModelEvent.updatePercent(name, percent),
);
}
},
onPluginPercentage: (percent) {
if (!isClosed) {
add(DownloadModelEvent.updatePercent("AppFlowy Plugin", percent));
}
},
onFinish: () {
add(const DownloadModelEvent.downloadFinish());
},
onError: (err) {
// emit(state.copyWith(downloadError: err));
},
);
final payload =
DownloadLLMPB(progressStream: Int64(downloadStream.nativePort));
final result = await ChatEventDownloadLLMResource(payload).send();
result.fold((_) {
emit(
state.copyWith(
downloadStream: downloadStream,
loadingState: const LoadingState.finish(),
downloadError: null,
),
);
}, (err) {
emit(state.copyWith(loadingState: LoadingState.finish(error: err)));
});
},
updatePercent: (String object, double percent) {
emit(state.copyWith(object: object, percent: percent));
},
downloadFinish: () {
emit(state.copyWith(isFinish: true));
},
);
}
}
@freezed
class DownloadModelEvent with _$DownloadModelEvent {
const factory DownloadModelEvent.started() = _Started;
const factory DownloadModelEvent.updatePercent(
String object,
double percent,
) = _UpdatePercent;
const factory DownloadModelEvent.downloadFinish() = _DownloadFinish;
}
@freezed
class DownloadModelState with _$DownloadModelState {
const factory DownloadModelState({
required LLMModelPB model,
DownloadingStream? downloadStream,
String? downloadError,
@Default("") String object,
@Default(0) double percent,
@Default(false) bool isFinish,
@Default(LoadingState.loading()) LoadingState loadingState,
}) = _DownloadModelState;
}
class DownloadingStream {
DownloadingStream() {
_port.handler = _controller.add;
}
final RawReceivePort _port = RawReceivePort();
StreamSubscription<String>? _sub;
final StreamController<String> _controller = StreamController.broadcast();
int get nativePort => _port.sendPort.nativePort;
Future<void> dispose() async {
await _sub?.cancel();
await _controller.close();
_port.close();
}
void listen({
void Function(String modelName, double percent)? onModelPercentage,
void Function(double percent)? onPluginPercentage,
void Function(String data)? onError,
void Function()? onFinish,
}) {
_sub = _controller.stream.listen((text) {
if (text.contains(':progress:')) {
final progressIndex = text.indexOf(':progress:');
final modelName = text.substring(0, progressIndex);
final progressValue = text
.substring(progressIndex + 10); // 10 is the length of ":progress:"
final percent = double.tryParse(progressValue);
if (percent != null) {
onModelPercentage?.call(modelName, percent);
}
} else if (text.startsWith('plugin:progress:')) {
final percent = double.tryParse(text.substring(16));
if (percent != null) {
onPluginPercentage?.call(percent);
}
} else if (text.startsWith('finish')) {
onFinish?.call();
} else if (text.startsWith('error:')) {
// substring 6 to remove "error:"
onError?.call(text.substring(6));
}
});
}
}
@@ -0,0 +1,253 @@
import 'dart:async';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'local_ai_bloc.freezed.dart';
class LocalAISettingBloc
extends Bloc<LocalAISettingEvent, LocalAISettingState> {
LocalAISettingBloc()
: listener = LocalLLMListener(),
super(const LocalAISettingState()) {
listener.start(
stateCallback: (newState) {
if (!isClosed) {
add(LocalAISettingEvent.updateLLMRunningState(newState.state));
}
},
);
on<LocalAISettingEvent>(_handleEvent);
}
final LocalLLMListener listener;
/// Handles incoming events and dispatches them to the appropriate handler.
Future<void> _handleEvent(
LocalAISettingEvent event,
Emitter<LocalAISettingState> emit,
) async {
await event.when(
started: _handleStarted,
didLoadModelInfo: (FlowyResult<LLMModelInfoPB, FlowyError> result) {
result.fold(
(modelInfo) {
_fetchCurremtLLMState();
emit(
state.copyWith(
modelInfo: modelInfo,
models: modelInfo.models,
selectedLLMModel: modelInfo.selectedModel,
fetchModelInfoState: const LoadingState.finish(),
),
);
},
(err) {
emit(
state.copyWith(
fetchModelInfoState: LoadingState.finish(error: err),
),
);
},
);
},
selectLLMConfig: (LLMModelPB llmModel) async {
final result = await ChatEventUpdateLocalLLM(llmModel).send();
result.fold(
(llmResource) {
// If all resources are downloaded, show reload plugin
if (llmResource.pendingResources.isNotEmpty) {
emit(
state.copyWith(
selectedLLMModel: llmModel,
localAIInfo: LocalAIProgress.showDownload(
llmResource,
llmModel,
),
selectLLMState: const LoadingState.finish(),
),
);
} else {
emit(
state.copyWith(
selectedLLMModel: llmModel,
selectLLMState: const LoadingState.finish(),
localAIInfo: const LocalAIProgress.checkPluginState(),
),
);
}
},
(err) {
emit(
state.copyWith(
selectLLMState: LoadingState.finish(error: err),
),
);
},
);
},
refreshLLMState: (LocalModelResourcePB llmResource) {
if (state.selectedLLMModel == null) {
Log.error(
'Unexpected null selected config. It should be set already',
);
return;
}
// reload plugin if all resources are downloaded
if (llmResource.pendingResources.isEmpty) {
emit(
state.copyWith(
localAIInfo: const LocalAIProgress.checkPluginState(),
),
);
} else {
if (state.selectedLLMModel != null) {
// Go to download page if the selected model is downloading
if (llmResource.isDownloading) {
emit(
state.copyWith(
localAIInfo:
LocalAIProgress.startDownloading(state.selectedLLMModel!),
selectLLMState: const LoadingState.finish(),
),
);
return;
} else {
emit(
state.copyWith(
localAIInfo: LocalAIProgress.showDownload(
llmResource,
state.selectedLLMModel!,
),
selectLLMState: const LoadingState.finish(),
),
);
}
}
}
},
startDownloadModel: (LLMModelPB llmModel) {
emit(
state.copyWith(
localAIInfo: LocalAIProgress.startDownloading(llmModel),
selectLLMState: const LoadingState.finish(),
),
);
},
cancelDownload: () async {
final _ = await ChatEventCancelDownloadLLMResource().send();
_fetchCurremtLLMState();
},
finishDownload: () async {
emit(
state.copyWith(localAIInfo: const LocalAIProgress.finishDownload()),
);
},
updateLLMRunningState: (RunningStatePB newRunningState) {
if (newRunningState == RunningStatePB.Stopped) {
emit(
state.copyWith(
runningState: newRunningState,
localAIInfo: const LocalAIProgress.checkPluginState(),
),
);
} else {
emit(state.copyWith(runningState: newRunningState));
}
},
);
}
void _fetchCurremtLLMState() async {
final result = await ChatEventGetLocalLLMState().send();
result.fold(
(llmResource) {
if (!isClosed) {
add(LocalAISettingEvent.refreshLLMState(llmResource));
}
},
(err) {
Log.error(err);
},
);
}
/// Handles the event to fetch local AI settings when the application starts.
Future<void> _handleStarted() async {
final result = await ChatEventRefreshLocalAIModelInfo().send();
if (!isClosed) {
add(LocalAISettingEvent.didLoadModelInfo(result));
}
}
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
}
@freezed
class LocalAISettingEvent with _$LocalAISettingEvent {
const factory LocalAISettingEvent.started() = _Started;
const factory LocalAISettingEvent.didLoadModelInfo(
FlowyResult<LLMModelInfoPB, FlowyError> result,
) = _ModelInfo;
const factory LocalAISettingEvent.selectLLMConfig(LLMModelPB config) =
_SelectLLMConfig;
const factory LocalAISettingEvent.refreshLLMState(
LocalModelResourcePB llmResource,
) = _RefreshLLMResource;
const factory LocalAISettingEvent.startDownloadModel(LLMModelPB llmModel) =
_StartDownloadModel;
const factory LocalAISettingEvent.cancelDownload() = _CancelDownload;
const factory LocalAISettingEvent.finishDownload() = _FinishDownload;
const factory LocalAISettingEvent.updateLLMRunningState(
RunningStatePB newRunningState,
) = _RunningState;
}
@freezed
class LocalAISettingState with _$LocalAISettingState {
const factory LocalAISettingState({
LLMModelInfoPB? modelInfo,
LLMModelPB? selectedLLMModel,
LocalAIProgress? localAIInfo,
@Default(LoadingState.loading()) LoadingState fetchModelInfoState,
@Default(LoadingState.loading()) LoadingState selectLLMState,
@Default([]) List<LLMModelPB> models,
@Default(RunningStatePB.Connecting) RunningStatePB runningState,
}) = _LocalAISettingState;
}
@freezed
class LocalAIProgress with _$LocalAIProgress {
// when user select a new model, it will call requestDownload
const factory LocalAIProgress.requestDownloadInfo(
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) = _RequestDownload;
// when user comes back to the setting page, it will auto detect current llm state
const factory LocalAIProgress.showDownload(
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) = _DownloadNeeded;
// when start downloading the model
const factory LocalAIProgress.startDownloading(LLMModelPB llmModel) =
_Downloading;
const factory LocalAIProgress.finishDownload() = _Finish;
const factory LocalAIProgress.checkPluginState() = _PluginState;
}
@@ -0,0 +1,54 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/plugins/ai_chat/application/chat_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
typedef PluginStateCallback = void Function(PluginStatePB state);
class LocalLLMListener {
LocalLLMListener() {
_parser =
ChatNotificationParser(id: "appflowy_chat_plugin", callback: _callback);
_subscription = RustStreamReceiver.listen(
(observable) => _parser?.parse(observable),
);
}
StreamSubscription<SubscribeObject>? _subscription;
ChatNotificationParser? _parser;
PluginStateCallback? stateCallback;
void Function()? finishStreamingCallback;
void start({
PluginStateCallback? stateCallback,
}) {
this.stateCallback = stateCallback;
}
void _callback(
ChatNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
result.map((r) {
switch (ty) {
case ChatNotification.UpdateChatPluginState:
stateCallback?.call(PluginStatePB.fromBuffer(r));
break;
default:
break;
}
});
}
Future<void> stop() async {
await _subscription?.cancel();
_subscription = null;
}
}
@@ -0,0 +1,88 @@
import 'dart:async';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'plugin_state_bloc.freezed.dart';
class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
PluginStateBloc()
: listener = LocalLLMListener(),
super(const PluginStateState(action: PluginStateAction.init())) {
listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(PluginStateEvent.updateState(pluginState));
}
},
);
on<PluginStateEvent>(_handleEvent);
}
final LocalLLMListener listener;
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
Future<void> _handleEvent(
PluginStateEvent event,
Emitter<PluginStateState> emit,
) async {
await event.when(
started: () async {
final result = await ChatEventGetPluginState().send();
result.fold(
(pluginState) {
if (!isClosed) {
add(PluginStateEvent.updateState(pluginState));
}
},
(err) => Log.error(err.toString()),
);
},
updateState: (PluginStatePB pluginState) {
switch (pluginState.state) {
case RunningStatePB.Running:
emit(const PluginStateState(action: PluginStateAction.ready()));
break;
default:
emit(
state.copyWith(action: const PluginStateAction.reloadRequired()),
);
break;
}
},
restartLocalAI: () {
ChatEventRestartLocalAI().send();
},
);
}
}
@freezed
class PluginStateEvent with _$PluginStateEvent {
const factory PluginStateEvent.started() = _Started;
const factory PluginStateEvent.updateState(PluginStatePB pluginState) =
_UpdatePluginState;
const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI;
}
@freezed
class PluginStateState with _$PluginStateState {
const factory PluginStateState({required PluginStateAction action}) =
_PluginStateState;
}
@freezed
class PluginStateAction with _$PluginStateAction {
const factory PluginStateAction.init() = _Init;
const factory PluginStateAction.ready() = _Ready;
const factory PluginStateAction.reloadRequired() = _ReloadRequired;
}
@@ -1,195 +0,0 @@
import 'dart:io';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as path;
import 'package:protobuf/protobuf.dart';
part 'setting_local_ai_bloc.freezed.dart';
class SettingsAILocalBloc
extends Bloc<SettingsAILocalEvent, SettingsAILocalState> {
SettingsAILocalBloc() : super(const SettingsAILocalState()) {
on<SettingsAILocalEvent>(_handleEvent);
}
/// Handles incoming events and dispatches them to the appropriate handler.
Future<void> _handleEvent(
SettingsAILocalEvent event,
Emitter<SettingsAILocalState> emit,
) async {
await event.when(
started: _handleStarted,
didUpdateAISetting: (settings) async {
_handleDidUpdateAISetting(settings, emit);
},
updateChatBin: (chatBinPath) async {
await _handleUpdatePath(
filePath: chatBinPath,
emit: emit,
stateUpdater: () => state.copyWith(
chatBinPath: chatBinPath.trim(),
chatBinPathError: null,
),
errorUpdater: (error) => state.copyWith(chatBinPathError: error),
);
},
updateChatModelPath: (chatModelPath) async {
await _handleUpdatePath(
filePath: chatModelPath,
emit: emit,
stateUpdater: () => state.copyWith(
chatModelPath: chatModelPath.trim(),
chatModelPathError: null,
),
errorUpdater: (error) => state.copyWith(chatModelPathError: error),
);
},
toggleLocalAI: () async {
emit(state.copyWith(localAIEnabled: !state.localAIEnabled));
},
saveSetting: () async {
_handleSaveSetting();
},
);
}
/// Handles the event to fetch local AI settings when the application starts.
Future<void> _handleStarted() async {
final result = await ChatEventGetLocalAISetting().send();
result.fold(
(setting) {
if (!isClosed) {
add(SettingsAILocalEvent.didUpdateAISetting(setting));
}
},
(err) => Log.error('Failed to get local AI setting: $err'),
);
}
/// Handles the event to update the AI settings in the state.
void _handleDidUpdateAISetting(
LocalLLMSettingPB settings,
Emitter<SettingsAILocalState> emit,
) {
final newState = state.copyWith(
aiSettings: settings,
chatBinPath: settings.chatBinPath,
chatModelPath: settings.chatModelPath,
localAIEnabled: settings.enabled,
loadingState: const LoadingState.finish(),
);
emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState)));
}
/// Handles updating file paths (both chat binary and chat model paths).
Future<void> _handleUpdatePath({
required String filePath,
required Emitter<SettingsAILocalState> emit,
required SettingsAILocalState Function() stateUpdater,
required SettingsAILocalState Function(String) errorUpdater,
}) async {
filePath = filePath.trim();
if (filePath.isEmpty) {
emit(stateUpdater());
return;
}
final validationError = await _validatePath(filePath);
if (validationError != null) {
emit(errorUpdater(validationError));
return;
}
final newState = stateUpdater();
emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState)));
}
/// Validates the provided file path.
Future<String?> _validatePath(String filePath) async {
if (!isAbsolutePath(filePath)) {
return "$filePath must be absolute";
}
if (!await pathExists(filePath)) {
return "$filePath does not exist";
}
return null;
}
/// Handles saving the updated settings.
void _handleSaveSetting() {
if (state.aiSettings == null) return;
state.aiSettings!.freeze();
final newSetting = state.aiSettings!.rebuild((value) {
value
..chatBinPath = state.chatBinPath ?? value.chatBinPath
..chatModelPath = state.chatModelPath ?? value.chatModelPath
..enabled = state.localAIEnabled;
});
ChatEventUpdateLocalAISetting(newSetting).send().then((result) {
result.fold(
(_) {
if (!isClosed) {
add(SettingsAILocalEvent.didUpdateAISetting(newSetting));
}
},
(err) => Log.error('Failed to update local AI setting: $err'),
);
});
}
/// Determines if the save button should be enabled based on the state.
bool _saveButtonEnabled(SettingsAILocalState newState) {
return newState.chatBinPathError == null &&
newState.chatModelPathError == null &&
newState.chatBinPath != null &&
newState.chatModelPath != null;
}
}
@freezed
class SettingsAILocalEvent with _$SettingsAILocalEvent {
const factory SettingsAILocalEvent.started() = _Started;
const factory SettingsAILocalEvent.didUpdateAISetting(
LocalLLMSettingPB settings,
) = _GetAISetting;
const factory SettingsAILocalEvent.updateChatBin(String chatBinPath) =
_UpdateChatBin;
const factory SettingsAILocalEvent.updateChatModelPath(String chatModelPath) =
_UpdateChatModelPath;
const factory SettingsAILocalEvent.toggleLocalAI() = _EnableLocalAI;
const factory SettingsAILocalEvent.saveSetting() = _SaveSetting;
}
@freezed
class SettingsAILocalState with _$SettingsAILocalState {
const factory SettingsAILocalState({
LocalLLMSettingPB? aiSettings,
String? chatBinPath,
String? chatBinPathError,
String? chatModelPath,
String? chatModelPathError,
@Default(false) bool localAIEnabled,
@Default(false) bool saveButtonEnabled,
@Default(LoadingState.loading()) LoadingState loadingState,
}) = _SettingsAILocalState;
}
/// Checks if a given file path is absolute.
bool isAbsolutePath(String filePath) {
return path.isAbsolute(filePath);
}
/// Checks if a given file or directory path exists.
Future<bool> pathExists(String filePath) async {
final file = File(filePath);
final directory = Directory(filePath);
return await file.exists() || await directory.exists();
}
@@ -141,7 +141,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
if (deletedSpace == null) {
return;
}
await ViewBackendService.delete(viewId: deletedSpace.id);
await ViewBackendService.deleteView(viewId: deletedSpace.id);
},
rename: (space, name) async {
add(SpaceEvent.update(name: name));
@@ -433,6 +433,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
workspaceId: workspaceId,
)..start(
sectionChanged: (result) async {
Log.info('did receive section views changed');
add(const SpaceEvent.didReceiveSpaceUpdate());
},
);
@@ -503,6 +504,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
if (_workspaceId == null) {
return false;
}
try {
final user =
await UserBackendService.getCurrentUserProfile().getOrThrow();
@@ -526,6 +528,13 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
(e) => e.isSpace && e.spacePermission == SpacePermission.publicToAll,
);
publicViews = publicViews.where((e) => !e.isSpace).toList();
for (final view in publicViews) {
Log.info(
'migrating: the public view should be migrated: ${view.name}(${view.id})',
);
}
// if there is already a public space, don't migrate the public space
// only migrate the public space if there are any public views
if (publicViews.isEmpty || containsPublicSpace) {
@@ -568,6 +577,13 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
(e) => e.isSpace && e.spacePermission == SpacePermission.private,
);
privateViews = privateViews.where((e) => !e.isSpace).toList();
for (final view in privateViews) {
Log.info(
'migrating: the private view should be migrated: ${view.name}(${view.id})',
);
}
if (privateViews.isEmpty || containsPrivateSpace) {
return true;
}
@@ -143,7 +143,7 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
// unpublish the page and all its child pages if they are published
await _unpublishPage(view);
final result = await ViewBackendService.delete(viewId: view.id);
final result = await ViewBackendService.deleteView(viewId: view.id);
emit(
result.fold(
@@ -122,17 +122,17 @@ class ViewBackendService {
});
}
static Future<FlowyResult<void, FlowyError>> delete({
static Future<FlowyResult<void, FlowyError>> deleteView({
required String viewId,
}) {
final request = RepeatedViewIdPB.create()..items.add(viewId);
return FolderEventDeleteView(request).send();
}
static Future<FlowyResult<void, FlowyError>> deleteView({
required String viewId,
static Future<FlowyResult<void, FlowyError>> deleteViews({
required List<String> viewIds,
}) {
final request = RepeatedViewIdPB.create()..items.add(viewId);
final request = RepeatedViewIdPB.create()..items.addAll(viewIds);
return FolderEventDeleteView(request).send();
}
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
@@ -8,6 +6,7 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RecentViewsList extends StatelessWidget {
@@ -24,7 +23,7 @@ class RecentViewsList extends StatelessWidget {
builder: (context, state) {
// We remove duplicates by converting the list to a set first
final List<ViewPB> recentViews =
state.views.reversed.map((e) => e.item).toSet().toList();
state.views.map((e) => e.item).toSet().toList();
return ListView.separated(
shrinkWrap: true,
@@ -4,11 +4,11 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/share/import_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -220,7 +220,7 @@ class _ImportPanelState extends State<ImportPanel> {
Uint8List? _documentDataFrom(ImportType importType, String data) {
switch (importType) {
case ImportType.markdownOrText:
final document = markdownToDocument(data);
final document = customMarkdownToDocument(data);
return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
case ImportType.historyDocument:
final document = EditorMigration.migrateDocument(data);
@@ -27,7 +27,6 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_migration.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
show UserProfilePB;
@@ -371,11 +370,6 @@ class _SidebarState extends State<_Sidebar> {
final sidebarSectionBloc = context.watch<SidebarSectionsBloc>();
final containsSpace = sidebarSectionBloc.state.containsSpace;
Log.info('fetch the space info from sidebar section: $containsSpace');
Log.info(
'fetch the space info from space: ${spaceState.spaces.isNotEmpty}',
);
if (containsSpace && spaceState.spaces.isEmpty) {
context.read<SpaceBloc>().add(const SpaceEvent.didReceiveSpaceUpdate());
}
@@ -1,5 +1,6 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
@@ -223,23 +224,96 @@ class SpaceCancelOrConfirmButton extends StatelessWidget {
}
}
class ConfirmDeletionPopup extends StatefulWidget {
const ConfirmDeletionPopup({
class SpaceOkButton extends StatelessWidget {
const SpaceOkButton({
super.key,
required this.onConfirm,
required this.confirmButtonName,
this.confirmButtonColor,
});
final VoidCallback onConfirm;
final String confirmButtonName;
final Color? confirmButtonColor;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
DecoratedBox(
decoration: ShapeDecoration(
color: confirmButtonColor ?? Theme.of(context).colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: FlowyButton(
useIntrinsicWidth: true,
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 9.0),
radius: BorderRadius.circular(8),
text: FlowyText.regular(
confirmButtonName,
color: Colors.white,
),
onTap: onConfirm,
),
),
],
);
}
}
enum ConfirmPopupStyle {
onlyOk,
cancelAndOk,
}
class ConfirmPopupColor {
static Color titleColor(BuildContext context) {
if (Theme.of(context).isLightMode) {
return const Color(0xFF171717).withOpacity(0.8);
}
return const Color(0xFFffffff).withOpacity(0.8);
}
static Color descriptionColor(BuildContext context) {
if (Theme.of(context).isLightMode) {
return const Color(0xFF171717).withOpacity(0.8);
}
return const Color(0xFFffffff).withOpacity(0.72);
}
}
class ConfirmPopup extends StatefulWidget {
const ConfirmPopup({
super.key,
this.style = ConfirmPopupStyle.cancelAndOk,
required this.title,
required this.description,
required this.onConfirm,
this.confirmLabel,
this.confirmButtonColor,
});
final String title;
final String description;
final VoidCallback onConfirm;
final Color? confirmButtonColor;
final ConfirmPopupStyle style;
/// The label of the confirm button.
///
/// Defaults to 'Delete' for [ConfirmPopupStyle.cancelAndOk] style.
/// Defaults to 'Ok' for [ConfirmPopupStyle.onlyOk] style.
///
final String? confirmLabel;
@override
State<ConfirmDeletionPopup> createState() => _ConfirmDeletionPopupState();
State<ConfirmPopup> createState() => _ConfirmPopupState();
}
class _ConfirmDeletionPopupState extends State<ConfirmDeletionPopup> {
class _ConfirmPopupState extends State<ConfirmPopup> {
final focusNode = FocusNode();
@override
@@ -262,46 +336,74 @@ class _ConfirmDeletionPopupState extends State<ConfirmDeletionPopup> {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Flexible(
child: FlowyText(
widget.title,
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
),
),
const HSpace(6.0),
FlowyButton(
useIntrinsicWidth: true,
text: const FlowySvg(FlowySvgs.upgrade_close_s),
onTap: () => Navigator.of(context).pop(),
),
],
),
const VSpace(8.0),
FlowyText.regular(
widget.description,
fontSize: 12.0,
color: Theme.of(context).hintColor,
maxLines: 3,
lineHeight: 1.4,
),
_buildTitle(),
const VSpace(6.0),
_buildDescription(),
const VSpace(20.0),
SpaceCancelOrConfirmButton(
onCancel: () => Navigator.of(context).pop(),
onConfirm: () {
widget.onConfirm();
Navigator.of(context).pop();
},
confirmButtonName: LocaleKeys.space_delete.tr(),
confirmButtonColor: Theme.of(context).colorScheme.error,
),
_buildStyledButton(context),
],
),
),
);
}
Widget _buildTitle() {
return Row(
children: [
Expanded(
child: FlowyText(
widget.title,
fontSize: 14.0,
overflow: TextOverflow.ellipsis,
color: ConfirmPopupColor.titleColor(context),
),
),
const HSpace(6.0),
FlowyButton(
useIntrinsicWidth: true,
text: const FlowySvg(FlowySvgs.upgrade_close_s),
onTap: () => Navigator.of(context).pop(),
),
],
);
}
Widget _buildDescription() {
return FlowyText.regular(
widget.description,
fontSize: 12.0,
color: ConfirmPopupColor.descriptionColor(context),
maxLines: 3,
lineHeight: 1.4,
);
}
Widget _buildStyledButton(BuildContext context) {
switch (widget.style) {
case ConfirmPopupStyle.onlyOk:
return SpaceOkButton(
onConfirm: () {
widget.onConfirm();
Navigator.of(context).pop();
},
confirmButtonName: widget.confirmLabel ?? LocaleKeys.button_ok.tr(),
confirmButtonColor: widget.confirmButtonColor ??
Theme.of(context).colorScheme.primary,
);
case ConfirmPopupStyle.cancelAndOk:
return SpaceCancelOrConfirmButton(
onCancel: () => Navigator.of(context).pop(),
onConfirm: () {
widget.onConfirm();
Navigator.of(context).pop();
},
confirmButtonName:
widget.confirmLabel ?? LocaleKeys.space_delete.tr(),
confirmButtonColor:
widget.confirmButtonColor ?? Theme.of(context).colorScheme.error,
);
}
}
}
class SpacePopup extends StatelessWidget {
@@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/shared/single_setting_a
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -24,7 +25,7 @@ class FixDataWidget extends StatelessWidget {
.tr(),
buttonLabel: LocaleKeys.settings_manageDataPage_data_fixButton.tr(),
onPressed: () {
FixDataManager.checkWorkspaceHealth(dryRun: true);
WorkspaceDataManager.checkWorkspaceHealth(dryRun: true);
},
),
],
@@ -32,7 +33,7 @@ class FixDataWidget extends StatelessWidget {
}
}
class FixDataManager {
class WorkspaceDataManager {
static Future<void> checkWorkspaceHealth({
required bool dryRun,
}) async {
@@ -63,6 +64,9 @@ class FixDataManager {
// check the health of the spaces
await checkSpaceHealth(workspace: workspace, allViews: allViews);
// check the health of the views
await checkViewHealth(workspace: workspace, allViews: allViews);
// add other checks here
// ...
} catch (e) {
@@ -83,7 +87,6 @@ class FixDataManager {
workspaceChildViews.map((e) => e.id).toSet();
final spaces = allViews.where((e) => e.isSpace).toList();
//
for (final space in spaces) {
// the space is the top level view, so its parent view id should be the workspace id
// and the workspace should have the space in its child views
@@ -106,6 +109,93 @@ class FixDataManager {
}
}
static Future<List<ViewPB>> checkViewHealth({
ViewPB? workspace,
List<ViewPB>? allViews,
bool dryRun = true,
}) async {
// Views whose parent view does not have the view in its child views
final List<ViewPB> unlistedChildViews = [];
// Views whose parent is not in allViews
final List<ViewPB> orphanViews = [];
try {
if (workspace == null || allViews == null) {
final currentWorkspace =
await UserBackendService.getCurrentWorkspace().getOrThrow();
// get all the views in the workspace
final result = await ViewBackendService.getAllViews().getOrThrow();
allViews = result.items;
workspace = allViews.firstWhereOrNull(
(e) => e.id == currentWorkspace.id,
);
}
for (final view in allViews) {
if (view.parentViewId == '') {
continue;
}
final parentView = allViews.firstWhereOrNull(
(e) => e.id == view.parentViewId,
);
if (parentView == null) {
orphanViews.add(view);
continue;
}
final childViewsOfParent =
await ViewBackendService.getChildViews(viewId: parentView.id)
.getOrThrow();
final result = childViewsOfParent.any((e) => e.id == view.id);
if (!result) {
unlistedChildViews.add(view);
}
}
} catch (e) {
Log.error('Failed to check space health: $e');
return [];
}
for (final view in unlistedChildViews) {
Log.info(
'[workspace] found an issue: view is not in the parent view\'s child views, view: ${view.toProto3Json()}}',
);
}
for (final view in orphanViews) {
Log.debug('[workspace] orphanViews: ${view.toProto3Json()}');
}
if (!dryRun && unlistedChildViews.isNotEmpty) {
Log.info(
'[workspace] start to fix ${unlistedChildViews.length} unlistedChildViews ...',
);
for (final view in unlistedChildViews) {
// move the view to the parent view if it is not in the parent view's child views
Log.info(
'[workspace] move view: $view to its parent view ${view.parentViewId}',
);
await ViewBackendService.moveViewV2(
viewId: view.id,
newParentId: view.parentViewId,
prevViewId: null,
);
}
Log.info('[workspace] end to fix unlistedChildViews');
}
if (unlistedChildViews.isEmpty && orphanViews.isEmpty) {
Log.info('[workspace] all views are healthy');
}
Log.info('[workspace] done checking view health');
return unlistedChildViews;
}
static void dumpViews(String prefix, List<ViewPB> views) {
for (int i = 0; i < views.length; i++) {
final view = views[i];
@@ -0,0 +1,108 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/download_model_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class DownloadingIndicator extends StatelessWidget {
const DownloadingIndicator({
required this.llmModel,
required this.onCancel,
required this.onFinish,
super.key,
});
final LLMModelPB llmModel;
final VoidCallback onCancel;
final VoidCallback onFinish;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
DownloadModelBloc(llmModel)..add(const DownloadModelEvent.started()),
child: BlocListener<DownloadModelBloc, DownloadModelState>(
listener: (context, state) {
if (state.isFinish) {
onFinish();
}
},
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
// const DownloadingPrompt(),
// const VSpace(12),
DownloadingProgressBar(onCancel: onCancel),
],
),
),
),
),
);
}
}
class DownloadingProgressBar extends StatelessWidget {
const DownloadingProgressBar({required this.onCancel, super.key});
final VoidCallback onCancel;
@override
Widget build(BuildContext context) {
return BlocBuilder<DownloadModelBloc, DownloadModelState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
"${LocaleKeys.settings_aiPage_keys_downloadingModel.tr()}: ${state.object}",
fontSize: 11,
),
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: LinearPercentIndicator(
lineHeight: 9.0,
percent: state.percent,
padding: EdgeInsets.zero,
progressColor: AFThemeExtension.of(context).success,
backgroundColor:
AFThemeExtension.of(context).progressBarBGColor,
barRadius: const Radius.circular(8),
trailing: FlowyText(
"${(state.percent * 100).toStringAsFixed(0)}%",
fontSize: 11,
color: AFThemeExtension.of(context).success,
),
),
),
const HSpace(12),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 11,
),
onTap: onCancel,
),
],
),
),
],
);
},
);
}
}
@@ -0,0 +1,75 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class InitLocalAIIndicator extends StatelessWidget {
const InitLocalAIIndicator({super.key});
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFFEDF7ED),
borderRadius: BorderRadius.all(
Radius.circular(4),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
switch (state.runningState) {
case RunningStatePB.Connecting:
case RunningStatePB.Connected:
return Row(
children: [
const HSpace(8),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoading.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
],
);
case RunningStatePB.Running:
return Row(
children: [
const HSpace(8),
const FlowySvg(
FlowySvgs.download_success_s,
color: Color(0xFF2E7D32),
),
const HSpace(6),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoaded.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
],
);
case RunningStatePB.Stopped:
return Row(
children: [
const HSpace(8),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAIStopped.tr(),
fontSize: 11,
color: const Color(0xFFC62828),
),
],
);
default:
return const SizedBox.shrink();
}
},
),
),
);
}
}
@@ -0,0 +1,314 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/downloading.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class LocalModelConfig extends StatelessWidget {
const LocalModelConfig({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
if (state.aiSettings == null) {
return const SizedBox.shrink();
}
if (state.aiSettings!.aiModel != AIModelPB.LocalAIModel) {
return const SizedBox.shrink();
}
return BlocProvider(
create: (context) =>
LocalAISettingBloc()..add(const LocalAISettingEvent.started()),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModel.tr(),
fontSize: 14,
),
),
const Spacer(),
BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
return state.fetchModelInfoState.when(
loading: () =>
const CircularProgressIndicator.adaptive(),
finish: (err) {
return (err == null)
? const _SelectLocalModelDropdownMenu()
: const SizedBox.shrink();
},
);
},
),
],
),
const IntrinsicHeight(child: _LocalLLMInfoWidget()),
],
),
),
);
},
);
}
}
class _SelectLocalModelDropdownMenu extends StatelessWidget {
const _SelectLocalModelDropdownMenu();
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
return Flexible(
child: SettingsDropdown<LLMModelPB>(
key: const Key('_SelectLocalModelDropdownMenu'),
onChanged: (model) => context.read<LocalAISettingBloc>().add(
LocalAISettingEvent.selectLLMConfig(model),
),
selectedOption: state.selectedLLMModel!,
options: state.models
.map(
(llm) => buildDropdownMenuEntry<LLMModelPB>(
context,
value: llm,
label: llm.chatModel,
padding: const EdgeInsets.symmetric(vertical: 8),
),
)
.toList(),
),
);
},
);
}
}
class _LocalLLMInfoWidget extends StatelessWidget {
const _LocalLLMInfoWidget();
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
final error = errorFromState(state);
if (error == null) {
// If the error is null, handle selected llm model.
if (state.localAIInfo != null) {
final child = state.localAIInfo!.when(
requestDownloadInfo: (
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) {
_showDownloadDialog(context, llmResource, llmModel);
return const SizedBox.shrink();
},
showDownload: (
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) =>
_ShowDownloadIndicator(
llmResource: llmResource,
llmModel: llmModel,
),
startDownloading: (llmModel) {
return DownloadingIndicator(
key: UniqueKey(),
llmModel: llmModel,
onFinish: () => context
.read<LocalAISettingBloc>()
.add(const LocalAISettingEvent.finishDownload()),
onCancel: () => context
.read<LocalAISettingBloc>()
.add(const LocalAISettingEvent.cancelDownload()),
);
},
finishDownload: () => const InitLocalAIIndicator(),
checkPluginState: () => const CheckPluginStateIndicator(),
);
return Padding(
padding: const EdgeInsets.only(top: 14),
child: child,
);
} else {
return const SizedBox.shrink();
}
} else {
return FlowyText(
error.msg,
maxLines: 10,
);
}
},
);
}
void _showDownloadDialog(
BuildContext context,
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) {
return _LLMModelDownloadDialog(
llmResource: llmResource,
onOkPressed: () {
context.read<LocalAISettingBloc>().add(
LocalAISettingEvent.startDownloadModel(
llmModel,
),
);
},
onCancelPressed: () {
context.read<LocalAISettingBloc>().add(
const LocalAISettingEvent.cancelDownload(),
);
},
);
},
);
},
debugLabel: 'localModel.download',
);
}
FlowyError? errorFromState(LocalAISettingState state) {
final err = state.fetchModelInfoState.when(
loading: () => null,
finish: (err) => err,
);
if (err == null) {
state.selectLLMState.when(
loading: () => null,
finish: (err) => err,
);
}
return err;
}
}
class _LLMModelDownloadDialog extends StatelessWidget {
const _LLMModelDownloadDialog({
required this.llmResource,
required this.onOkPressed,
required this.onCancelPressed,
});
final LocalModelResourcePB llmResource;
final VoidCallback onOkPressed;
final VoidCallback onCancelPressed;
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr(
args: [
llmResource.pendingResources[0].name,
],
),
message: llmResource.pendingResources[0].fileSize == 0
? ""
: LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr(
args: [
llmResource.pendingResources[0].name,
llmResource.pendingResources[0].fileSize.toString(),
],
),
okTitle: LocaleKeys.button_confirm.tr(),
cancelTitle: LocaleKeys.button_cancel.tr(),
onOkPressed: onOkPressed,
onCancelPressed: onCancelPressed,
titleUpperCase: false,
);
}
}
class _ShowDownloadIndicator extends StatelessWidget {
const _ShowDownloadIndicator({
required this.llmResource,
required this.llmModel,
});
final LocalModelResourcePB llmResource;
final LLMModelPB llmModel;
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
return Row(
children: [
const Spacer(),
IntrinsicWidth(
child: SizedBox(
height: 30,
child: FlowyButton(
text: FlowyText(
LocaleKeys.settings_aiPage_keys_downloadAIModelButton.tr(),
fontSize: 14,
color: const Color(0xFF005483),
),
leftIcon: const FlowySvg(
FlowySvgs.local_model_download_s,
color: Color(0xFF005483),
),
onTap: () {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) {
return _LLMModelDownloadDialog(
llmResource: llmResource,
onOkPressed: () {
context.read<LocalAISettingBloc>().add(
LocalAISettingEvent.startDownloadModel(
llmModel,
),
);
},
onCancelPressed: () {
context.read<LocalAISettingBloc>().add(
const LocalAISettingEvent.cancelDownload(),
);
},
);
},
);
},
),
),
),
],
);
},
);
}
}
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIModelSelection extends StatelessWidget {
const AIModelSelection({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModelType.tr(),
fontSize: 14,
),
),
const Spacer(),
Flexible(
child: SettingsDropdown<AIModelPB>(
key: const Key('_AIModelSelection'),
onChanged: (model) => context
.read<SettingsAIBloc>()
.add(SettingsAIEvent.selectModel(model)),
selectedOption: state.userProfile.aiModel,
options: _availableModels
.map(
(format) => buildDropdownMenuEntry<AIModelPB>(
context,
value: format,
label: _titleForAIModel(format),
),
)
.toList(),
),
),
],
),
);
},
);
}
}
List<AIModelPB> _availableModels = [
AIModelPB.DefaultModel,
AIModelPB.Claude3Opus,
AIModelPB.Claude3Sonnet,
AIModelPB.GPT35,
AIModelPB.GPT4o,
// AIModelPB.LocalAIModel,
];
String _titleForAIModel(AIModelPB model) {
switch (model) {
case AIModelPB.DefaultModel:
return "Default";
case AIModelPB.Claude3Opus:
return "Claude 3 Opus";
case AIModelPB.Claude3Sonnet:
return "Claude 3 Sonnet";
case AIModelPB.GPT35:
return "GPT-3.5";
case AIModelPB.GPT4o:
return "GPT-4o";
case AIModelPB.LocalAIModel:
return "Local";
default:
Log.error("Unknown AI model: $model, fallback to default");
return "Default";
}
}
@@ -0,0 +1,111 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.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';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CheckPluginStateIndicator extends StatelessWidget {
const CheckPluginStateIndicator({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
PluginStateBloc()..add(const PluginStateEvent.started()),
child: BlocBuilder<PluginStateBloc, PluginStateState>(
builder: (context, state) {
return state.action.when(
init: () => const _InitPlugin(),
ready: () => const _ReadyToUse(),
reloadRequired: () => const _ReloadButton(),
);
},
),
);
}
}
class _InitPlugin extends StatelessWidget {
const _InitPlugin();
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 20,
child: CircularProgressIndicator.adaptive(),
);
}
}
class _ReloadButton extends StatelessWidget {
const _ReloadButton();
@override
Widget build(BuildContext context) {
return Row(
children: [
const FlowySvg(
FlowySvgs.download_warn_s,
color: Color(0xFFC62828),
),
const HSpace(6),
FlowyText(LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr()),
const Spacer(),
SizedBox(
height: 30,
child: FlowyButton(
useIntrinsicWidth: true,
text:
FlowyText(LocaleKeys.settings_aiPage_keys_restartLocalAI.tr()),
onTap: () {
context.read<PluginStateBloc>().add(
const PluginStateEvent.restartLocalAI(),
);
},
),
),
],
);
}
}
class _ReadyToUse extends StatelessWidget {
const _ReadyToUse();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: DecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFFEDF7ED),
borderRadius: BorderRadius.all(
Radius.circular(4),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
const HSpace(8),
const FlowySvg(
FlowySvgs.download_success_s,
color: Color(0xFF2E7D32),
),
const HSpace(6),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoaded.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
],
),
),
),
);
}
}
@@ -0,0 +1,107 @@
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget {
const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30),
child: FlowyText(
LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(),
maxLines: null,
fontSize: 16,
lineHeight: 1.6,
),
);
}
}
class SettingsAIView extends StatelessWidget {
const SettingsAIView({super.key, required this.userProfile});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsAIBloc>(
create: (_) =>
SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
final children = <Widget>[
const AIModelSelection(),
];
if (state.aiSettings != null &&
state.aiSettings!.aiModel == AIModelPB.LocalAIModel) {
children.add(const LocalModelConfig());
}
children.add(const _AISearchToggle(value: false));
return SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
description:
LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(),
children: children,
);
},
),
);
}
}
class _AISearchToggle extends StatelessWidget {
const _AISearchToggle({required this.value});
final bool value;
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
FlowyText.medium(
LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(),
),
const Spacer(),
BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
if (state.aiSettings == null) {
return const Padding(
padding: EdgeInsets.only(top: 6),
child: SizedBox(
height: 26,
width: 26,
child: CircularProgressIndicator.adaptive(),
),
);
} else {
return Toggle(
value: state.enableSearchIndexing,
onChanged: (_) => context
.read<SettingsAIBloc>()
.add(const SettingsAIEvent.toggleAISearch()),
);
}
},
),
],
),
],
);
}
}
@@ -1,279 +0,0 @@
import 'package:appflowy/workspace/application/settings/ai/setting_local_ai_bloc.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget {
const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30),
child: FlowyText(
LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(),
maxLines: null,
fontSize: 16,
lineHeight: 1.6,
),
);
}
}
class SettingsAIView extends StatelessWidget {
const SettingsAIView({super.key, required this.userProfile});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsAIBloc>(
create: (_) =>
SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
return SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
description:
LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(),
children: const [
AIModelSelection(),
_AISearchToggle(value: false),
// Disable local AI configuration for now. It's not ready for production.
LocalAIConfiguration(),
],
);
},
),
);
}
}
class AIModelSelection extends StatelessWidget {
const AIModelSelection({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModel.tr(),
),
),
const Spacer(),
Flexible(
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
return SettingsDropdown<AIModelPB>(
key: const Key('AIModelDropdown'),
onChanged: (model) => context
.read<SettingsAIBloc>()
.add(SettingsAIEvent.selectModel(model)),
selectedOption: state.userProfile.aiModel,
options: _availableModels
.map(
(format) => buildDropdownMenuEntry<AIModelPB>(
context,
value: format,
label: _titleForAIModel(format),
),
)
.toList(),
);
},
),
),
],
);
}
}
List<AIModelPB> _availableModels = [
AIModelPB.DefaultModel,
AIModelPB.Claude3Opus,
AIModelPB.Claude3Sonnet,
AIModelPB.GPT35,
AIModelPB.GPT4o,
];
String _titleForAIModel(AIModelPB model) {
switch (model) {
case AIModelPB.DefaultModel:
return "Default";
case AIModelPB.Claude3Opus:
return "Claude 3 Opus";
case AIModelPB.Claude3Sonnet:
return "Claude 3 Sonnet";
case AIModelPB.GPT35:
return "GPT-3.5";
case AIModelPB.GPT4o:
return "GPT-4o";
case AIModelPB.LocalAIModel:
return "Local";
default:
Log.error("Unknown AI model: $model, fallback to default");
return "Default";
}
}
class _AISearchToggle extends StatelessWidget {
const _AISearchToggle({required this.value});
final bool value;
@override
Widget build(BuildContext context) {
return Row(
children: [
FlowyText.medium(
LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(),
),
const Spacer(),
BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
if (state.aiSettings == null) {
return const Padding(
padding: EdgeInsets.only(top: 6),
child: SizedBox(
height: 26,
width: 26,
child: CircularProgressIndicator.adaptive(),
),
);
} else {
return Toggle(
value: state.enableSearchIndexing,
onChanged: (_) => context
.read<SettingsAIBloc>()
.add(const SettingsAIEvent.toggleAISearch()),
);
}
},
),
],
);
}
}
class LocalAIConfiguration extends StatelessWidget {
const LocalAIConfiguration({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
SettingsAILocalBloc()..add(const SettingsAILocalEvent.started()),
child: BlocBuilder<SettingsAILocalBloc, SettingsAILocalState>(
builder: (context, state) {
return state.loadingState.when(
loading: () {
return const SizedBox.shrink();
},
finish: () {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AIConfigurateTextField(
title: 'chat bin path',
hitText: '',
errorText: state.chatBinPathError ?? '',
value: state.aiSettings?.chatBinPath ?? '',
onChanged: (value) {
context.read<SettingsAILocalBloc>().add(
SettingsAILocalEvent.updateChatBin(value),
);
},
),
const VSpace(16),
AIConfigurateTextField(
title: 'chat model path',
hitText: '',
errorText: state.chatModelPathError ?? '',
value: state.aiSettings?.chatModelPath ?? '',
onChanged: (value) {
context.read<SettingsAILocalBloc>().add(
SettingsAILocalEvent.updateChatModelPath(value),
);
},
),
const VSpace(16),
Toggle(
value: state.localAIEnabled,
onChanged: (_) => context
.read<SettingsAILocalBloc>()
.add(const SettingsAILocalEvent.toggleLocalAI()),
),
const VSpace(16),
FlowyButton(
disable: !state.saveButtonEnabled,
text: const FlowyText("save"),
onTap: () {
context.read<SettingsAILocalBloc>().add(
const SettingsAILocalEvent.saveSetting(),
);
},
),
],
);
},
);
},
),
);
}
}
class AIConfigurateTextField extends StatelessWidget {
const AIConfigurateTextField({
required this.title,
required this.hitText,
required this.errorText,
required this.value,
required this.onChanged,
super.key,
});
final String title;
final String hitText;
final String errorText;
final String value;
final void Function(String) onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
title,
),
const VSpace(8),
RoundedInputField(
hintText: hitText,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
normalBorderColor: Theme.of(context).colorScheme.outline,
errorBorderColor: Theme.of(context).colorScheme.error,
cursorColor: Theme.of(context).colorScheme.primary,
errorText: errorText,
initialValue: value,
onChanged: onChanged,
),
],
);
}
}
@@ -126,26 +126,34 @@ class SettingsManageDataView extends StatelessWidget {
buttonLabel:
LocaleKeys.settings_manageDataPage_cache_title.tr(),
onPressed: () {
SettingsAlertDialog(
showCancelAndConfirmDialog(
context: context,
title: LocaleKeys
.settings_manageDataPage_cache_dialog_title
.tr(),
subtitle: LocaleKeys
description: LocaleKeys
.settings_manageDataPage_cache_dialog_description
.tr(),
confirm: () async {
confirmLabel: LocaleKeys.button_ok.tr(),
onConfirm: () async {
// clear all cache
await getIt<FlowyCacheManager>().clearAllCache();
// check the workspace and space health
await WorkspaceDataManager.checkViewHealth(
dryRun: false,
);
if (context.mounted) {
showSnackBarMessage(
showToastNotification(
context,
LocaleKeys
message: LocaleKeys
.settings_manageDataPage_cache_dialog_successHint
.tr(),
);
Navigator.of(context).pop();
}
},
).show(context);
);
},
),
],
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/theme_extension.dart';
@@ -12,6 +10,7 @@ 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/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsPlanComparisonDialog extends StatefulWidget {
@@ -173,9 +172,10 @@ class _SettingsPlanComparisonDialogState
description: LocaleKeys
.settings_comparePlanDialog_freePlan_description
.tr(),
// TODO(Mathias): the price should be dynamic based on the country and currency
price: LocaleKeys
.settings_comparePlanDialog_freePlan_price
.tr(),
.tr(args: ['\$0']),
priceInfo: LocaleKeys
.settings_comparePlanDialog_freePlan_priceInfo
.tr(),
@@ -231,9 +231,10 @@ class _SettingsPlanComparisonDialogState
description: LocaleKeys
.settings_comparePlanDialog_proPlan_description
.tr(),
// TODO(Mathias): the price should be dynamic based on the country and currency
price: LocaleKeys
.settings_comparePlanDialog_proPlan_price
.tr(),
.tr(args: ['\$10 ']),
priceInfo: LocaleKeys
.settings_comparePlanDialog_proPlan_priceInfo
.tr(),
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/int64_extension.dart';
@@ -19,6 +17,7 @@ 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/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsPlanView extends StatelessWidget {
@@ -710,7 +709,7 @@ class _PlanProgressIndicator extends StatelessWidget {
// children: [
// FlowyText.semibold(
// LocaleKeys.settings_planPage_planUsage_aiCredit_price
// .tr(),
// .tr(args: ['5\$]),
// fontSize: 24,
// ),
// FlowyText.medium(
@@ -15,6 +15,7 @@ import 'package:appflowy/workspace/application/settings/date_time/date_format_ex
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
@@ -31,6 +32,7 @@ import 'package:appflowy/workspace/presentation/settings/shared/settings_input_f
import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart';
import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@@ -44,7 +46,6 @@ import 'package:flowy_infra/theme.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:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -1144,9 +1145,21 @@ class _DocumentCursorColorSetting extends StatelessWidget {
return SettingListTile(
label: label,
resetButtonKey: const Key('DocumentCursorColorResetButton'),
onResetRequested: () => context
..read<AppearanceSettingsCubit>().resetDocumentCursorColor()
..read<DocumentAppearanceCubit>().syncCursorColor(null),
onResetRequested: () {
showConfirmDialog(
context: context,
title:
LocaleKeys.settings_workspacePage_resetCursorColor_title.tr(),
description: LocaleKeys
.settings_workspacePage_resetCursorColor_description
.tr(),
style: ConfirmPopupStyle.cancelAndOk,
confirmLabel: LocaleKeys.settings_common_reset.tr(),
onConfirm: () => context
..read<AppearanceSettingsCubit>().resetDocumentCursorColor()
..read<DocumentAppearanceCubit>().syncCursorColor(null),
);
},
trailing: [
DocumentColorSettingButton(
key: const Key('DocumentCursorColorSettingButton'),
@@ -1202,9 +1215,21 @@ class _DocumentSelectionColorSetting extends StatelessWidget {
return SettingListTile(
label: label,
resetButtonKey: const Key('DocumentSelectionColorResetButton'),
onResetRequested: () => context
..read<AppearanceSettingsCubit>().resetDocumentSelectionColor()
..read<DocumentAppearanceCubit>().syncSelectionColor(null),
onResetRequested: () {
showConfirmDialog(
context: context,
title: LocaleKeys.settings_workspacePage_resetSelectionColor_title
.tr(),
description: LocaleKeys
.settings_workspacePage_resetSelectionColor_description
.tr(),
style: ConfirmPopupStyle.cancelAndOk,
confirmLabel: LocaleKeys.settings_common_reset.tr(),
onConfirm: () => context
..read<AppearanceSettingsCubit>().resetDocumentSelectionColor()
..read<DocumentAppearanceCubit>().syncSelectionColor(null),
);
},
trailing: [
DocumentColorSettingButton(
currentColor: state.selectionColor ??
@@ -4,7 +4,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_ai_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart';
@@ -13,6 +13,7 @@ DropdownMenuEntry<T> buildDropdownMenuEntry<T>(
Widget? leadingWidget,
Widget? trailingWidget,
String? fontFamily,
EdgeInsets padding = const EdgeInsets.symmetric(vertical: 4),
}) {
final fontFamilyUsed = fontFamily != null
? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily
@@ -32,7 +33,7 @@ DropdownMenuEntry<T> buildDropdownMenuEntry<T>(
label: label,
leadingIcon: leadingWidget,
labelWidget: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
padding: padding,
child: FlowyText.regular(
label,
fontSize: 14,
@@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
@@ -90,24 +94,11 @@ class DocumentColorSettingDialogState
late TextEditingController hexController;
late TextEditingController opacityController;
void updateSelectedColor() {
if (widget.formKey.currentState!.validate()) {
setState(() {
final colorValue = int.tryParse(
hexController.text.combineHexWithOpacity(opacityController.text),
);
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
selectedColorOnDialog = Color(colorValue!);
widget.onChanged(selectedColorOnDialog!);
});
}
}
@override
void initState() {
super.initState();
selectedColorOnDialog = widget.currentColor;
currentColorHexString = widget.currentColor.toHexString();
currentColorHexString = ColorExtension(widget.currentColor).toHexString();
hexController = TextEditingController(
text: currentColorHexString.extractHex(),
);
@@ -145,17 +136,31 @@ class DocumentColorSettingDialogState
controller: hexController,
labelText: LocaleKeys.editor_hexValue.tr(),
hintText: '6fc9e7',
onChanged: (_) => updateSelectedColor(),
onFieldSubmitted: (_) => updateSelectedColor(),
onChanged: (_) => _updateSelectedColor(),
onFieldSubmitted: (_) => _updateSelectedColor(),
validator: (v) => validateHexValue(v, opacityController.text),
suffixIcon: Padding(
padding: const EdgeInsets.all(6.0),
child: FlowyIconButton(
onPressed: () => _showColorPickerDialog(
context: context,
currentColor: widget.currentColor,
updateColor: _updateColor,
),
icon: const FlowySvg(
FlowySvgs.m_aa_color_s,
size: Size.square(20),
),
),
),
),
const VSpace(8),
_ColorSettingTextField(
controller: opacityController,
labelText: LocaleKeys.editor_opacity.tr(),
hintText: '50',
onChanged: (_) => updateSelectedColor(),
onFieldSubmitted: (_) => updateSelectedColor(),
onChanged: (_) => _updateSelectedColor(),
onFieldSubmitted: (_) => _updateSelectedColor(),
validator: (value) => validateOpacityValue(value),
),
],
@@ -164,6 +169,28 @@ class DocumentColorSettingDialogState
],
);
}
void _updateSelectedColor() {
if (widget.formKey.currentState!.validate()) {
setState(() {
final colorValue = int.tryParse(
hexController.text.combineHexWithOpacity(opacityController.text),
);
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
selectedColorOnDialog = Color(colorValue!);
widget.onChanged(selectedColorOnDialog!);
});
}
}
void _updateColor(Color color) {
setState(() {
hexController.text = ColorExtension(color).toHexString().extractHex();
opacityController.text =
ColorExtension(color).toHexString().extractOpacity();
});
_updateSelectedColor();
}
}
class _ColorSettingTextField extends StatelessWidget {
@@ -172,6 +199,7 @@ class _ColorSettingTextField extends StatelessWidget {
required this.labelText,
required this.hintText,
required this.onFieldSubmitted,
this.suffixIcon,
this.onChanged,
this.validator,
});
@@ -180,6 +208,7 @@ class _ColorSettingTextField extends StatelessWidget {
final String labelText;
final String hintText;
final void Function(String) onFieldSubmitted;
final Widget? suffixIcon;
final void Function(String)? onChanged;
final String? Function(String?)? validator;
@@ -191,6 +220,7 @@ class _ColorSettingTextField extends StatelessWidget {
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderSide: BorderSide(color: style.colorScheme.outline),
),
@@ -241,3 +271,159 @@ String? validateOpacityValue(String? value) {
}
return null;
}
const _kColorCircleWidth = 32.0;
const _kColorCircleHeight = 32.0;
const _kColorCircleRadius = 20.0;
const _kColorOpacityThumbRadius = 23.0;
const _kDialogButtonPaddingHorizontal = 24.0;
const _kDialogButtonPaddingVertical = 12.0;
const _kColorsColumnSpacing = 12.0;
class _ColorPicker extends StatelessWidget {
const _ColorPicker({
required this.selectedColor,
required this.onColorChanged,
});
final Color selectedColor;
final void Function(Color) onColorChanged;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ColorPicker(
width: _kColorCircleWidth,
height: _kColorCircleHeight,
borderRadius: _kColorCircleRadius,
enableOpacity: true,
opacityThumbRadius: _kColorOpacityThumbRadius,
columnSpacing: _kColorsColumnSpacing,
enableTooltips: false,
hasBorder: true,
borderColor: theme.colorScheme.outline,
pickersEnabled: const {
ColorPickerType.both: false,
ColorPickerType.primary: true,
ColorPickerType.accent: true,
ColorPickerType.wheel: true,
},
subheading: Text(
LocaleKeys.settings_appearance_documentSettings_colorShade.tr(),
style: theme.textTheme.labelLarge,
),
opacitySubheading: Text(
LocaleKeys.settings_appearance_documentSettings_opacity.tr(),
style: theme.textTheme.labelLarge,
),
onColorChanged: onColorChanged,
);
}
}
class _ColorPickerActions extends StatelessWidget {
const _ColorPickerActions({
required this.onReset,
required this.onUpdate,
});
final VoidCallback onReset;
final VoidCallback onUpdate;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 24,
child: FlowyTextButton(
LocaleKeys.button_cancel.tr(),
padding: const EdgeInsets.symmetric(
horizontal: _kDialogButtonPaddingHorizontal,
vertical: _kDialogButtonPaddingVertical,
),
fontColor: AFThemeExtension.of(context).textColor,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
radius: Corners.s12Border,
onPressed: onReset,
),
),
const HSpace(8),
SizedBox(
height: 48,
child: FlowyTextButton(
LocaleKeys.button_done.tr(),
padding: const EdgeInsets.symmetric(
horizontal: _kDialogButtonPaddingHorizontal,
vertical: _kDialogButtonPaddingVertical,
),
radius: Corners.s12Border,
fontHoverColor: Colors.white,
fillColor: Theme.of(context).colorScheme.primary,
hoverColor: const Color(0xFF005483),
onPressed: onUpdate,
),
),
],
);
}
}
void _showColorPickerDialog({
required BuildContext context,
String? title,
required Color currentColor,
required void Function(Color) updateColor,
}) {
Color selectedColor = currentColor;
showDialog(
context: context,
barrierColor: const Color.fromARGB(128, 0, 0, 0),
builder: (context) => FlowyDialog(
expandHeight: false,
title: Row(
children: [
const FlowySvg(FlowySvgs.m_aa_color_s),
const HSpace(12),
FlowyText(
title ??
LocaleKeys.settings_appearance_documentSettings_pickColor.tr(),
fontSize: 20,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_ColorPicker(
selectedColor: selectedColor,
onColorChanged: (color) => selectedColor = color,
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const HSpace(8),
_ColorPickerActions(
onReset: () {
updateColor(currentColor);
Navigator.of(context).pop();
},
onUpdate: () {
updateColor(selectedColor);
Navigator.of(context).pop();
},
),
],
),
),
],
),
),
);
}
@@ -1,3 +1,5 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
@@ -9,7 +11,6 @@ import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
import 'package:flowy_infra_ui/widget/buttons/secondary_button.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:toastification/toastification.dart';
export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
@@ -184,6 +185,7 @@ class NavigatorOkCancelDialog extends StatelessWidget {
this.title,
this.message,
this.maxWidth,
this.titleUpperCase = true,
});
final VoidCallback? onOkPressed;
@@ -193,6 +195,7 @@ class NavigatorOkCancelDialog extends StatelessWidget {
final String? title;
final String? message;
final double? maxWidth;
final bool titleUpperCase;
@override
Widget build(BuildContext context) {
@@ -204,7 +207,7 @@ class NavigatorOkCancelDialog extends StatelessWidget {
children: <Widget>[
if (title != null) ...[
FlowyText.medium(
title!.toUpperCase(),
titleUpperCase ? title!.toUpperCase() : title!,
fontSize: FontSizes.s16,
maxLines: 3,
),
@@ -289,10 +292,11 @@ void showToastNotification(
BuildContext context, {
required String message,
String? description,
ToastificationType type = ToastificationType.success,
}) {
toastification.show(
context: context,
type: ToastificationType.success,
type: type,
style: ToastificationStyle.flat,
title: FlowyText(message),
description: description != null
@@ -329,7 +333,7 @@ Future<void> showConfirmDeletionDialog({
),
child: SizedBox(
width: 440,
child: ConfirmDeletionPopup(
child: ConfirmPopup(
title: title,
description: description,
onConfirm: onConfirm,
@@ -339,3 +343,62 @@ Future<void> showConfirmDeletionDialog({
},
);
}
Future<void> showConfirmDialog({
required BuildContext context,
required String title,
required String description,
VoidCallback? onConfirm,
String? confirmLabel,
ConfirmPopupStyle style = ConfirmPopupStyle.onlyOk,
}) {
return showDialog(
context: context,
builder: (_) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: SizedBox(
width: 440,
child: ConfirmPopup(
title: title,
description: description,
onConfirm: () => onConfirm?.call(),
confirmLabel: confirmLabel,
style: style,
),
),
);
},
);
}
Future<void> showCancelAndConfirmDialog({
required BuildContext context,
required String title,
required String description,
VoidCallback? onConfirm,
String? confirmLabel,
}) {
return showDialog(
context: context,
builder: (_) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
child: SizedBox(
width: 440,
child: ConfirmPopup(
title: title,
description: description,
onConfirm: () => onConfirm?.call(),
confirmLabel: confirmLabel,
confirmButtonColor: Theme.of(context).colorScheme.primary,
),
),
);
},
);
}
@@ -1,9 +1,12 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/env/env.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';
import 'package:appflowy/startup/tasks/rust_sdk.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:device_info_plus/device_info_plus.dart';
@@ -203,9 +206,24 @@ class FlowyVersionDescription extends CustomActionCell {
thickness: 1.0,
),
const VSpace(6),
FlowyText(
"$appName $version",
color: Theme.of(context).hintColor,
GestureDetector(
behavior: HitTestBehavior.opaque,
onDoubleTap: () {
if (Env.internalBuild != '1') {
return;
}
enableDocumentInternalLog = !enableDocumentInternalLog;
showToastNotification(
context,
message: enableDocumentInternalLog
? 'Enabled Internal Log'
: 'Disabled Internal Log',
);
},
child: FlowyText(
'$appName $version',
color: Theme.of(context).hintColor,
),
),
],
).padding(
@@ -1,16 +1,19 @@
export 'package:async/async.dart';
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'dart:isolate';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:ffi/ffi.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'dart:ffi';
import 'ffi.dart' as ffi;
import 'package:ffi/ffi.dart';
import 'dart:isolate';
import 'dart:io';
import 'package:logger/logger.dart';
import 'ffi.dart' as ffi;
export 'package:async/async.dart';
enum ExceptionType {
AppearanceSettingsIsEmpty,
}
@@ -72,7 +75,8 @@ class RustLogStreamReceiver {
lineLength: 120, // width of the output
colors: false, // Colorful log messages
printEmojis: false, // Print an emoji for each log message
printTime: false, // Should each log print contain a timestamp
dateTimeFormat:
DateTimeFormat.none, // Should each log print contain a timestamp
),
level: kDebugMode ? Level.trace : Level.info,
);
@@ -1,5 +1,6 @@
// ignore: import_of_legacy_library_into_null_safe
import 'dart:ffi';
import 'package:ffi/ffi.dart' as ffi;
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
@@ -14,13 +15,15 @@ class Log {
Log() {
_logger = Logger(
printer: PrettyPrinter(
methodCount: 2, // number of method calls to be displayed
errorMethodCount: 8, // number of method calls if stacktrace is provided
lineLength: 120, // width of the output
colors: true, // Colorful log messages
printEmojis: true, // Print an emoji for each log message
printTime: false, // Should each log print contain a timestamp
),
methodCount: 2, // number of method calls to be displayed
errorMethodCount:
8, // number of method calls if stacktrace is provided
lineLength: 120, // width of the output
colors: true, // Colorful log messages
printEmojis: true, // Print an emoji for each log message
dateTimeFormat:
DateTimeFormat.none // Should each log print contain a timestamp
),
level: kDebugMode ? Level.trace : Level.info,
);
}
@@ -15,7 +15,7 @@ dependencies:
isolates: ^3.0.3+8
protobuf: ^3.1.0
freezed_annotation:
logger: ^2.0.0
logger: ^2.4.0
plugin_platform_interface: ^2.1.3
json_annotation: ^4.7.0
appflowy_result:
+87 -7
View File
@@ -53,11 +53,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: e8ee051
resolved-ref: e8ee051719eded6621ccdc2722f696411c020209
ref: e0d673a
resolved-ref: e0d673afbbbcaf9df0276f7e0b6405d8f6e98112
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "3.0.0"
version: "3.1.0"
appflowy_editor_plugins:
dependency: "direct main"
description:
@@ -121,6 +121,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
url: "https://pub.dev"
source: hosted
version: "2.2.8"
bidi:
dependency: transitive
description:
name: bidi
sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63"
url: "https://pub.dev"
source: hosted
version: "2.0.10"
bitsdojo_window:
dependency: "direct main"
description:
@@ -418,6 +434,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.7.10"
desktop_drop:
dependency: "direct main"
description:
name: desktop_drop
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
url: "https://pub.dev"
source: hosted
version: "0.4.4"
device_info_plus:
dependency: "direct main"
description:
@@ -586,6 +610,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
flex_color_picker:
dependency: "direct main"
description:
name: flex_color_picker
sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e"
url: "https://pub.dev"
source: hosted
version: "3.5.1"
flex_seed_scheme:
dependency: transitive
description:
name: flex_seed_scheme
sha256: "6c595e545b0678e1fe17e8eec3d1fbca7237482da194fadc20ad8607dc7a7f3d"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
flowy_infra:
dependency: "direct main"
description:
@@ -965,6 +1005,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.0"
image:
dependency: transitive
description:
name: image
sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8"
url: "https://pub.dev"
source: hosted
version: "4.2.0"
image_gallery_saver:
dependency: "direct main"
description:
@@ -1203,13 +1251,13 @@ packages:
source: hosted
version: "0.1.5"
logger:
dependency: transitive
dependency: "direct main"
description:
name: logger
sha256: "6bbb9d6f7056729537a4309bda2e74e18e5d9f14302489cc1e93f33b3fe32cac"
sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32"
url: "https://pub.dev"
source: hosted
version: "2.0.2+1"
version: "2.4.0"
logging:
dependency: transitive
description:
@@ -1219,7 +1267,7 @@ packages:
source: hosted
version: "1.2.0"
markdown:
dependency: transitive
dependency: "direct main"
description:
name: markdown
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
@@ -1434,6 +1482,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.0+3"
pdf:
dependency: transitive
description:
name: pdf
sha256: "81d5522bddc1ef5c28e8f0ee40b71708761753c163e0c93a40df56fd515ea0f0"
url: "https://pub.dev"
source: hosted
version: "3.11.0"
pdf_widget_wrapper:
dependency: transitive
description:
name: pdf_widget_wrapper
sha256: c930860d987213a3d58c7ec3b7ecf8085c3897f773e8dc23da9cae60a5d6d0f5
url: "https://pub.dev"
source: hosted
version: "1.0.4"
percent_indicator:
dependency: "direct main"
description:
@@ -1554,6 +1618,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
printing:
dependency: transitive
description:
name: printing
sha256: cc4b256a5a89d5345488e3318897b595867f5181b8c5ed6fc63bfa5f2044aec3
url: "https://pub.dev"
source: hosted
version: "5.13.1"
process:
dependency: transitive
description:
@@ -1594,6 +1666,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.3"
qr:
dependency: transitive
description:
name: qr
sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
realtime_client:
dependency: transitive
description:
+6 -2
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.3
version: 0.6.4
environment:
flutter: ">=3.22.0"
@@ -66,6 +66,7 @@ dependencies:
styled_widget: ^0.4.1
expandable: ^5.0.1
flutter_colorpicker: ^1.0.3
flex_color_picker: ^3.5.1
highlight: ^0.7.0
package_info_plus: ^6.0.0
url_launcher: ^6.1.11
@@ -141,6 +142,9 @@ dependencies:
shimmer: ^3.0.0
isolates: ^3.0.3+8
markdown_widget: ^2.3.2+6
desktop_drop: ^0.4.4
markdown:
logger: ^2.4.0
# Window Manager for MacOS and Linux
window_manager: ^0.3.9
@@ -187,7 +191,7 @@ dependency_overrides:
appflowy_editor:
git:
url: https://github.com/AppFlowy-IO/appflowy-editor.git
ref: "e8ee051"
ref: "e0d673a"
appflowy_editor_plugins:
git:
File diff suppressed because it is too large. Load diff
+12 -11
View File
@@ -29,7 +29,8 @@ tokio = "1.34.0"
tokio-stream = "0.1.14"
async-trait = "0.1.74"
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
yrs = "0.18.8"
zip = "2.1.3"
yrs = "0.19.1"
# Please use the following script to update collab.
# Working directory: frontend
#
@@ -52,7 +53,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eebdbcad79a35b07305affdd36f16d9ce95c5a18" }
[dependencies]
serde_json.workspace = true
@@ -115,17 +116,17 @@ custom-protocol = ["tauri/custom-protocol"]
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
# Working directory: frontend
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
+1 -1
View File
@@ -24,7 +24,7 @@
"coverage": "pnpm run test:unit && pnpm run test:components"
},
"dependencies": {
"@appflowyinc/client-api-wasm": "0.1.1",
"@appflowyinc/client-api-wasm": "0.1.2",
"@atlaskit/primitives": "^5.5.3",
"@emoji-mart/data": "^1.1.2",
"@emoji-mart/react": "^1.1.1",
+8 -8
View File
@@ -1,13 +1,9 @@
lockfileVersion: '6.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
dependencies:
'@appflowyinc/client-api-wasm':
specifier: 0.1.1
version: 0.1.1
specifier: 0.1.2
version: 0.1.2
'@atlaskit/primitives':
specifier: ^5.5.3
version: 5.7.0(@types/react@18.2.66)(react@18.2.0)
@@ -451,8 +447,8 @@ packages:
'@jridgewell/gen-mapping': 0.3.5
'@jridgewell/trace-mapping': 0.3.25
/@appflowyinc/client-api-wasm@0.1.1:
resolution: {integrity: sha512-7+/TCmzMi9KrxX3HFLJv9R6ON2AO5xQavV547ii7RZM8+5bZJakuf6+pnyCzOquQX07q3ZYwJCa3MIgDvficaA==}
/@appflowyinc/client-api-wasm@0.1.2:
resolution: {integrity: sha512-+v0hs7/7BVKtgev/Bcbr0u2HLDhUuw4ZvZTaMddI+06HK8vt5S52dMaZKUcMvh1eUjVX8hjC6Mfe0X/yHqvFgA==}
dev: false
/@atlaskit/analytics-next-stable-react-context@1.0.1(react@18.2.0):
@@ -11666,3 +11662,7 @@ packages:
resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==}
engines: {node: '>=12.20'}
dev: true
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
+277 -94
View File
@@ -163,7 +163,7 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "app-error"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"bincode",
@@ -183,7 +183,7 @@ dependencies = [
[[package]]
name = "appflowy-ai-client"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"bytes",
@@ -197,22 +197,26 @@ dependencies = [
[[package]]
name = "appflowy-local-ai"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=346020270a0a3d3c82a60b545cd3f52144d56beb#346020270a0a3d3c82a60b545cd3f52144d56beb"
dependencies = [
"anyhow",
"appflowy-plugin",
"bytes",
"reqwest",
"serde",
"serde_json",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"zip 2.1.3",
"zip-extensions",
]
[[package]]
name = "appflowy-plugin"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=0820a0d23f7b813dee505e7e29e88a8561699fe8#0820a0d23f7b813dee505e7e29e88a8561699fe8"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-LocalAI?rev=346020270a0a3d3c82a60b545cd3f52144d56beb#346020270a0a3d3c82a60b545cd3f52144d56beb"
dependencies = [
"anyhow",
"cfg-if",
@@ -226,6 +230,7 @@ dependencies = [
"tokio",
"tokio-stream",
"tracing",
"xattr",
]
[[package]]
@@ -254,6 +259,15 @@ dependencies = [
"uuid",
]
[[package]]
name = "arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "arboard"
version = "3.3.2"
@@ -388,6 +402,12 @@ version = "0.21.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567"
[[package]]
name = "base64"
version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "base64ct"
version = "1.6.0"
@@ -453,9 +473,9 @@ checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
[[package]]
name = "bitpacking"
version = "0.8.4"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8c7d2ac73c167c06af4a5f37e6e59d84148d57ccbe4480b76f0273eefea82d7"
checksum = "4c1d3e2bfd8d06048a179f7b17afc3188effa10385e7b00dc65af6aae732ea92"
dependencies = [
"crunchy",
]
@@ -544,9 +564,9 @@ dependencies = [
[[package]]
name = "bumpalo"
version = "3.15.4"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "bytecheck"
@@ -780,7 +800,7 @@ dependencies = [
[[package]]
name = "client-api"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"again",
"anyhow",
@@ -801,6 +821,7 @@ dependencies = [
"getrandom 0.2.12",
"gotrue",
"infra",
"lazy_static",
"mime",
"parking_lot 0.12.1",
"percent-encoding",
@@ -829,7 +850,7 @@ dependencies = [
[[package]]
name = "client-api-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"collab-entity",
"collab-rt-entity",
@@ -841,7 +862,7 @@ dependencies = [
[[package]]
name = "client-websocket"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"futures-channel",
"futures-util",
@@ -924,7 +945,7 @@ dependencies = [
[[package]]
name = "collab"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [
"anyhow",
"async-trait",
@@ -948,7 +969,7 @@ dependencies = [
[[package]]
name = "collab-database"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [
"anyhow",
"async-trait",
@@ -978,7 +999,7 @@ dependencies = [
[[package]]
name = "collab-document"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [
"anyhow",
"collab",
@@ -998,7 +1019,7 @@ dependencies = [
[[package]]
name = "collab-entity"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [
"anyhow",
"bytes",
@@ -1013,7 +1034,7 @@ dependencies = [
[[package]]
name = "collab-folder"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [
"anyhow",
"chrono",
@@ -1051,7 +1072,7 @@ dependencies = [
[[package]]
name = "collab-plugins"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [
"anyhow",
"async-stream",
@@ -1090,7 +1111,7 @@ dependencies = [
[[package]]
name = "collab-rt-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"bincode",
@@ -1115,7 +1136,7 @@ dependencies = [
[[package]]
name = "collab-rt-protocol"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"async-trait",
@@ -1132,7 +1153,7 @@ dependencies = [
[[package]]
name = "collab-user"
version = "0.2.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=5048762#5048762dbb01abcbe75237e86c0d090e2f1d7c23"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Collab?rev=20f7814#20f7814beb265ea76e85ea7a9d392b9fe18a2a63"
dependencies = [
"anyhow",
"collab",
@@ -1183,6 +1204,12 @@ version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
[[package]]
name = "constant_time_eq"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7144d30dcf0fafbce74250a3963025d8d52177934239851c917d29f1df280c2"
[[package]]
name = "convert_case"
version = "0.4.0"
@@ -1280,10 +1307,25 @@ dependencies = [
]
[[package]]
name = "crc32fast"
version = "1.4.0"
name = "crc"
version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
dependencies = [
"crc-catalog",
]
[[package]]
name = "crc-catalog"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
@@ -1476,7 +1518,7 @@ checksum = "7e962a19be5cfc3f3bf6dd8f61eb50107f356ad6270fbb3ed41476571db78be5"
[[package]]
name = "database-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"app-error",
@@ -1503,6 +1545,12 @@ dependencies = [
"regex",
]
[[package]]
name = "deflate64"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83ace6c86376be0b6cdcf3fb41882e81d94b31587573d1cfa9d01cd06bba210d"
[[package]]
name = "delegate-display"
version = "2.1.1"
@@ -1547,6 +1595,17 @@ dependencies = [
"syn 2.0.55",
]
[[package]]
name = "derive_arbitrary"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
]
[[package]]
name = "derive_more"
version = "0.99.17"
@@ -1671,6 +1730,17 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b"
[[package]]
name = "displaydoc"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
]
[[package]]
name = "dlib"
version = "0.5.2"
@@ -1898,9 +1968,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80"
[[package]]
name = "flate2"
version = "1.0.28"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide",
@@ -1923,6 +1993,7 @@ dependencies = [
"anyhow",
"appflowy-local-ai",
"appflowy-plugin",
"base64 0.21.7",
"bytes",
"dashmap",
"flowy-chat-pub",
@@ -1932,19 +2003,26 @@ dependencies = [
"flowy-notification",
"flowy-sqlite",
"futures",
"futures-util",
"lib-dispatch",
"lib-infra",
"log",
"md5",
"parking_lot 0.12.1",
"protobuf",
"reqwest",
"serde",
"serde_json",
"sha2",
"strum_macros 0.21.1",
"tokio",
"tokio-stream",
"tokio-util",
"tracing",
"uuid",
"validator",
"zip 2.1.3",
"zip-extensions",
]
[[package]]
@@ -2246,12 +2324,14 @@ dependencies = [
"flowy-notification",
"flowy-search-pub",
"flowy-sqlite",
"futures",
"lazy_static",
"lib-dispatch",
"lib-infra",
"nanoid",
"parking_lot 0.12.1",
"protobuf",
"regex",
"serde",
"serde_json",
"strum_macros 0.21.1",
@@ -2272,6 +2352,7 @@ dependencies = [
"collab-entity",
"collab-folder",
"lib-infra",
"serde",
"uuid",
]
@@ -2582,12 +2663,12 @@ dependencies = [
[[package]]
name = "fs4"
version = "0.6.6"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eeb4ed9e12f43b7fa0baae3f9cdda28352770132ef2e09a23760c29cae8bd47"
checksum = "f7e180ac76c23b45e767bd7ae9579bc0bb458618c4bc71835926e098e61d15f8"
dependencies = [
"rustix",
"windows-sys 0.48.0",
"windows-sys 0.52.0",
]
[[package]]
@@ -3014,7 +3095,7 @@ dependencies = [
[[package]]
name = "gotrue"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"futures-util",
@@ -3031,7 +3112,7 @@ dependencies = [
[[package]]
name = "gotrue-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"app-error",
@@ -3468,7 +3549,7 @@ dependencies = [
[[package]]
name = "infra"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"bytes",
@@ -3541,9 +3622,9 @@ dependencies = [
[[package]]
name = "itertools"
version = "0.11.0"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
dependencies = [
"either",
]
@@ -3732,7 +3813,7 @@ dependencies = [
"tracing",
"validator",
"walkdir",
"zip",
"zip 0.6.6",
]
[[package]]
@@ -3843,6 +3924,12 @@ dependencies = [
"scopeguard",
]
[[package]]
name = "lockfree-object-pool"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e"
[[package]]
name = "log"
version = "0.4.21"
@@ -3867,9 +3954,9 @@ dependencies = [
[[package]]
name = "lru"
version = "0.11.1"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a83fb7698b3643a0e34f9ae6f2e8f0178c0fd42f8b59d493aa271ff3a5bf21"
checksum = "d3262e75e648fce39813cb56ac41f3c3e3f65217ebf3844d818d1f9398cfb0dc"
dependencies = [
"hashbrown 0.14.3",
]
@@ -3880,6 +3967,16 @@ version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75761162ae2b0e580d7e7c390558127e5f01b4194debd6221fd8c207fc80e3f5"
[[package]]
name = "lzma-rs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e"
dependencies = [
"byteorder",
"crc",
]
[[package]]
name = "mac"
version = "0.1.1"
@@ -3989,15 +4086,15 @@ dependencies = [
[[package]]
name = "memchr"
version = "2.7.1"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memmap2"
version = "0.7.1"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f49388d20533534cd19360ad3d6a7dadc885944aa802ba3995040c5ec11288c6"
checksum = "fe751422e4a8caa417e13c3ea66452215d7d63e19e604f4980461212f3ae1322"
dependencies = [
"libc",
]
@@ -4228,6 +4325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
dependencies = [
"autocfg",
"libm",
]
[[package]]
@@ -4440,9 +4538,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "ownedbytes"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e8a72b918ae8198abb3a18c190288123e1d442b6b9a7d709305fd194688b4b7"
checksum = "c3a059efb063b8f425b948e042e6b9bd85edfe60e913630ed727b23e2dfcc558"
dependencies = [
"stable_deref_trait",
]
@@ -4978,7 +5076,7 @@ checksum = "c55e02e35260070b6f716a2423c2ff1c3bb1642ddca6f99e1f26d06268a0e2d2"
dependencies = [
"bytes",
"heck 0.4.1",
"itertools 0.11.0",
"itertools 0.10.5",
"log",
"multimap",
"once_cell",
@@ -4999,7 +5097,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efb6c9a1dd1def8e2124d17e83a20af56f1570d6c2d2bd9e266ccb768df3840e"
dependencies = [
"anyhow",
"itertools 0.11.0",
"itertools 0.10.5",
"proc-macro2",
"quote",
"syn 2.0.55",
@@ -5235,6 +5333,16 @@ dependencies = [
"getrandom 0.2.12",
]
[[package]]
name = "rand_distr"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31"
dependencies = [
"num-traits",
"rand 0.8.5",
]
[[package]]
name = "rand_hc"
version = "0.2.0"
@@ -5977,7 +6085,7 @@ dependencies = [
[[package]]
name = "shared-entity"
version = "0.1.0"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=9884d93aa2805013f36a79c1757174a0b5063065#9884d93aa2805013f36a79c1757174a0b5063065"
source = "git+https://github.com/AppFlowy-IO/AppFlowy-Cloud?rev=eebdbcad79a35b07305affdd36f16d9ce95c5a18#eebdbcad79a35b07305affdd36f16d9ce95c5a18"
dependencies = [
"anyhow",
"app-error",
@@ -6355,14 +6463,13 @@ dependencies = [
[[package]]
name = "tantivy"
version = "0.21.1"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6083cd777fa94271b8ce0fe4533772cb8110c3044bab048d20f70108329a1f2"
checksum = "f8d0582f186c0a6d55655d24543f15e43607299425c5ad8352c242b914b31856"
dependencies = [
"aho-corasick",
"arc-swap",
"async-trait",
"base64 0.21.7",
"base64 0.22.1",
"bitpacking",
"byteorder",
"census",
@@ -6370,16 +6477,16 @@ dependencies = [
"crossbeam-channel",
"downcast-rs",
"fastdivide",
"fnv",
"fs4",
"htmlescape",
"itertools 0.11.0",
"itertools 0.12.1",
"levenshtein_automata",
"log",
"lru",
"lz4_flex",
"measure_time",
"memmap2",
"murmurhash32",
"num_cpus",
"once_cell",
"oneshot",
@@ -6407,22 +6514,22 @@ dependencies = [
[[package]]
name = "tantivy-bitpacker"
version = "0.5.0"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecb164321482301f514dd582264fa67f70da2d7eb01872ccd71e35e0d96655a"
checksum = "284899c2325d6832203ac6ff5891b297fc5239c3dc754c5bc1977855b23c10df"
dependencies = [
"bitpacking",
]
[[package]]
name = "tantivy-columnar"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d85f8019af9a78b3118c11298b36ffd21c2314bd76bbcd9d12e00124cbb7e70"
checksum = "12722224ffbe346c7fec3275c699e508fd0d4710e629e933d5736ec524a1f44e"
dependencies = [
"downcast-rs",
"fastdivide",
"fnv",
"itertools 0.11.0",
"itertools 0.12.1",
"serde",
"tantivy-bitpacker",
"tantivy-common",
@@ -6432,9 +6539,9 @@ dependencies = [
[[package]]
name = "tantivy-common"
version = "0.6.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af4a3a975e604a2aba6b1106a04505e1e7a025e6def477fab6e410b4126471e1"
checksum = "8019e3cabcfd20a1380b491e13ff42f57bb38bf97c3d5fa5c07e50816e0621f4"
dependencies = [
"async-trait",
"byteorder",
@@ -6445,50 +6552,52 @@ dependencies = [
[[package]]
name = "tantivy-fst"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc3c506b1a8443a3a65352df6382a1fb6a7afe1a02e871cee0d25e2c3d5f3944"
checksum = "d60769b80ad7953d8a7b2c70cdfe722bbcdcac6bccc8ac934c40c034d866fc18"
dependencies = [
"byteorder",
"regex-syntax 0.6.29",
"regex-syntax 0.8.2",
"utf8-ranges",
]
[[package]]
name = "tantivy-query-grammar"
version = "0.21.0"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d39c5a03100ac10c96e0c8b07538e2ab8b17da56434ab348309b31f23fada77"
checksum = "847434d4af57b32e309f4ab1b4f1707a6c566656264caa427ff4285c4d9d0b82"
dependencies = [
"nom",
]
[[package]]
name = "tantivy-sstable"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0c1bb43e5e8b8e05eb8009610344dbf285f06066c844032fbb3e546b3c71df"
checksum = "c69578242e8e9fc989119f522ba5b49a38ac20f576fc778035b96cc94f41f98e"
dependencies = [
"tantivy-bitpacker",
"tantivy-common",
"tantivy-fst",
"zstd 0.12.4",
"zstd 0.13.2",
]
[[package]]
name = "tantivy-stacker"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2c078595413f13f218cf6f97b23dcfd48936838f1d3d13a1016e05acd64ed6c"
checksum = "c56d6ff5591fc332739b3ce7035b57995a3ce29a93ffd6012660e0949c956ea8"
dependencies = [
"murmurhash32",
"rand_distr",
"tantivy-common",
]
[[package]]
name = "tantivy-tokenizer-api"
version = "0.2.0"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "347b6fb212b26d3505d224f438e3c4b827ab8bd847fe9953ad5ac6b8f9443b66"
checksum = "2a0dcade25819a89cfe6f17d932c9cedff11989936bf6dd4f336d50392053b04"
dependencies = [
"serde",
]
@@ -6852,18 +6961,18 @@ checksum = "8eaa81235c7058867fa8c0e7314f33dcce9c215f535d1913822a2b3f5e289f3c"
[[package]]
name = "thiserror"
version = "1.0.58"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
checksum = "f2675633b1499176c2dff06b0856a27976a8f9d436737b4cf4f312d4d91d8bbb"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.58"
version = "1.0.62"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
checksum = "d20468752b09f49e909e55a5d338caa8bedf615594e9d80bc4c565d30faf798c"
dependencies = [
"proc-macro2",
"quote",
@@ -6904,9 +7013,9 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.34"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa 1.0.10",
@@ -6925,9 +7034,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.17"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
@@ -7044,16 +7153,19 @@ dependencies = [
[[package]]
name = "tokio-util"
version = "0.7.10"
version = "0.7.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15"
checksum = "9cf6b47b3771c49ac75ad09a6162f53ad4b8088b76ac60e8ec1455b31a189fe1"
dependencies = [
"bytes",
"futures-core",
"futures-io",
"futures-sink",
"futures-util",
"hashbrown 0.14.3",
"pin-project-lite",
"slab",
"tokio",
"tracing",
]
[[package]]
@@ -8445,9 +8557,9 @@ dependencies = [
[[package]]
name = "yrs"
version = "0.18.8"
version = "0.19.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da227d69095141c331d9b60c11496d0a3c6505cd9f8e200898b197219e8e394f"
checksum = "71df0198938b69f1eec0d5f19f591c6e4f2f770b0bf16f858428f6d91b8bb280"
dependencies = [
"arc-swap",
"atomic_refcell",
@@ -8479,6 +8591,26 @@ dependencies = [
"syn 2.0.55",
]
[[package]]
name = "zeroize"
version = "1.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
dependencies = [
"zeroize_derive",
]
[[package]]
name = "zeroize_derive"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.55",
]
[[package]]
name = "zip"
version = "0.6.6"
@@ -8488,7 +8620,7 @@ dependencies = [
"aes",
"byteorder",
"bzip2",
"constant_time_eq",
"constant_time_eq 0.1.5",
"crc32fast",
"crossbeam-utils",
"flate2",
@@ -8499,6 +8631,58 @@ dependencies = [
"zstd 0.11.2+zstd.1.5.2",
]
[[package]]
name = "zip"
version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "775a2b471036342aa69bc5a602bc889cb0a06cda00477d0c69566757d5553d39"
dependencies = [
"aes",
"arbitrary",
"bzip2",
"constant_time_eq 0.3.0",
"crc32fast",
"crossbeam-utils",
"deflate64",
"displaydoc",
"flate2",
"hmac",
"indexmap 2.2.6",
"lzma-rs",
"memchr",
"pbkdf2 0.12.2",
"rand 0.8.5",
"sha1",
"thiserror",
"time",
"zeroize",
"zopfli",
"zstd 0.13.2",
]
[[package]]
name = "zip-extensions"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb0a99499b3497d765525c5d05e3ade9ca4a731c184365c19472c3fd6ba86341"
dependencies = [
"zip 2.1.3",
]
[[package]]
name = "zopfli"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946"
dependencies = [
"bumpalo",
"crc32fast",
"lockfree-object-pool",
"log",
"once_cell",
"simd-adler32",
]
[[package]]
name = "zstd"
version = "0.11.2+zstd.1.5.2"
@@ -8510,11 +8694,11 @@ dependencies = [
[[package]]
name = "zstd"
version = "0.12.4"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe 6.0.6",
"zstd-safe 7.2.0",
]
[[package]]
@@ -8529,19 +8713,18 @@ dependencies = [
[[package]]
name = "zstd-safe"
version = "6.0.6"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581"
checksum = "fa556e971e7b568dc775c136fc9de8c779b1c2fc3a63defaafadffdbd3181afa"
dependencies = [
"libc",
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.9+zstd.1.5.5"
version = "2.0.12+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e16efa8a874a0481a574084d34cc26fdb3b99627480f785888deb6386506656"
checksum = "0a4e40c320c3cb459d9a9ff6de98cff88f4751ee9275d140e2be94a2b74e4c13"
dependencies = [
"cc",
"pkg-config",
+11 -11
View File
@@ -29,7 +29,7 @@ tokio = "1.34.0"
tokio-stream = "0.1.14"
async-trait = "0.1.74"
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
yrs = "0.18.8"
yrs = "0.19.1"
# Please use the following script to update collab.
# Working directory: frontend
#
@@ -52,7 +52,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eebdbcad79a35b07305affdd36f16d9ce95c5a18" }
[dependencies]
serde_json.workspace = true
@@ -116,18 +116,18 @@ custom-protocol = ["tauri/custom-protocol"]
# To switch to the local path, run:
# scripts/tool/update_collab_source.sh
# ⚠️⚠️⚠️️
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "5048762" }
collab = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-entity = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-folder = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-document = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-database = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-plugins = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
collab-user = { version = "0.2", git = "https://github.com/AppFlowy-IO/AppFlowy-Collab", rev = "20f7814" }
# Working directory: frontend
# To update the commit ID, run:
# scripts/tool/update_local_ai_rev.sh new_rev_id
# ⚠️⚠️⚠️️
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "0820a0d23f7b813dee505e7e29e88a8561699fe8" }
appflowy-local-ai = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
@@ -513,6 +513,7 @@ export function observeDeepRow(
export function useRowDataSelector(rowId: string) {
const rowMap = useRowDocMap();
const rowSharedRoot = rowMap?.get(rowId)?.getMap(YjsEditorKey.data_section);
const row = rowSharedRoot?.get(YjsEditorKey.database_row);
@@ -6,6 +6,8 @@ export type ViewMeta = {
child_views: PublishViewInfo[];
ancestor_views: PublishViewInfo[];
visible_view_ids: string[];
} & PublishViewInfo;
export type ViewMetasTable = {
@@ -9,6 +9,7 @@ import {
} from '@/application/services/js-services/cache';
import { openCollabDB, db } from '@/application/db';
import { StrategyType } from '@/application/services/js-services/cache/types';
import * as Y from 'yjs';
jest.mock('@/application/ydoc/apply', () => ({
applyYDoc: jest.fn(),
@@ -118,9 +119,8 @@ describe('Cache functions', () => {
describe('getBatchCollabs', () => {
it('should return empty array when no cache found', async () => {
(openCollabDB as jest.Mock).mockResolvedValue(undefined);
const collabs = await getBatchCollabs(['1', '2', '3']);
expect(collabs).toEqual([]);
(openCollabDB as jest.Mock).mockResolvedValue(new Y.Doc());
await expect(getBatchCollabs(['1', '2', '3'])).rejects.toThrow('No cache found');
});
it('should return collabs when cache found', async () => {
@@ -110,6 +110,8 @@ export async function getPublishViewMeta<
export async function getPublishView<
T extends {
data: number[];
rows?: Record<string, number[]>;
visibleViewIds?: string[];
meta: {
view: PublishViewInfo;
child_views: PublishViewInfo[];
@@ -176,12 +178,15 @@ export async function revalidatePublishViewMeta<
>(name: string, fetcher: Fetcher<T>) {
const { view, child_views, ancestor_views } = await fetcher();
const dbView = await db.view_metas.get(name);
await db.view_metas.put(
{
publish_name: name,
...view,
child_views: child_views,
ancestor_views: ancestor_views,
visible_view_ids: dbView?.visible_view_ids ?? [],
},
name
);
@@ -193,10 +198,11 @@ export async function revalidatePublishView<
T extends {
data: number[];
rows?: Record<string, number[]>;
visibleViewIds?: string[];
meta: PublishViewMetaData;
}
>(name: string, fetcher: Fetcher<T>, collab: YDoc) {
const { data, meta, rows } = await fetcher();
const { data, meta, rows, visibleViewIds = [] } = await fetcher();
await db.view_metas.put(
{
@@ -204,6 +210,7 @@ export async function revalidatePublishView<
...meta.view,
child_views: meta.child_views,
ancestor_views: meta.ancestor_views,
visible_view_ids: visibleViewIds,
},
name
);
@@ -222,7 +229,22 @@ export async function revalidatePublishView<
}
export async function getBatchCollabs(names: string[]) {
const collabs = await Promise.all(names.map((name) => openCollabDB(name)));
const getRowDoc = async (name: string) => {
const doc = await openCollabDB(name);
const exist = hasCollabCache(doc);
if (!exist) {
return Promise.reject(new Error('No cache found'));
}
return doc;
};
const collabs = await Promise.all(
names.map((name) => {
return getRowDoc(name);
})
);
return collabs;
}
@@ -8,9 +8,18 @@ import {
} from '@/application/services/js-services/cache';
import { StrategyType } from '@/application/services/js-services/cache/types';
import { fetchPublishView, fetchPublishViewMeta, fetchViewInfo } from '@/application/services/js-services/fetch';
import {
initAPIService,
signInGoogle,
signInWithMagicLink,
signInGithub,
signInDiscord,
signInWithUrl,
} from '@/application/services/js-services/wasm/client_api';
import { AFService, AFServiceConfig } from '@/application/services/services.type';
import { emit, EventType } from '@/application/session';
import { afterAuth, AUTH_CALLBACK_URL, withSignIn } from '@/application/session/sign_in';
import { nanoid } from 'nanoid';
import { initAPIService } from '@/application/services/js-services/wasm/client_api';
import * as Y from 'yjs';
export class AFClientService implements AFService {
@@ -38,6 +47,10 @@ export class AFClientService implements AFService {
});
}
getClientId() {
return this.clientId;
}
async getPublishViewMeta(namespace: string, publishName: string) {
const viewMeta = await getPublishViewMeta(
() => {
@@ -109,12 +122,13 @@ export class AFClientService implements AFService {
}
const rowsFolder: Y.Map<YDoc> = rootRowsDoc.getMap();
const docs = await getBatchCollabs(rowIds);
const docs = await getBatchCollabs(rowIds.map((id) => `${name}_${id}`));
docs.forEach((doc, index) => {
rowsFolder.set(rowIds[index], doc);
});
console.log('getPublishDatabaseViewRows', docs);
return {
rows: rowsFolder,
destroy: () => {
@@ -149,4 +163,37 @@ export class AFClientService implements AFService {
return data;
}
async loginAuth(url: string) {
try {
console.log('loginAuth', url);
await signInWithUrl(url);
emit(EventType.SESSION_VALID);
afterAuth();
return;
} catch (e) {
emit(EventType.SESSION_INVALID);
return Promise.reject(e);
}
}
@withSignIn()
async signInMagicLink({ email }: { email: string; redirectTo: string }) {
return await signInWithMagicLink(email, AUTH_CALLBACK_URL);
}
@withSignIn()
async signInGoogle(_: { redirectTo: string }) {
return await signInGoogle(AUTH_CALLBACK_URL);
}
@withSignIn()
async signInGithub(_: { redirectTo: string }) {
return await signInGithub(AUTH_CALLBACK_URL);
}
@withSignIn()
async signInDiscord(_: { redirectTo: string }) {
return await signInDiscord(AUTH_CALLBACK_URL);
}
}
@@ -1,6 +1,7 @@
import { getToken, invalidToken, isTokenValid, refreshToken } from '@/application/session/token';
import { ClientAPI } from '@appflowyinc/client-api-wasm';
import { AFCloudConfig } from '@/application/services/services.type';
import { PublishViewMetaData } from '@/application/collab.type';
import { PublishViewMetaData, ViewLayout } from '@/application/collab.type';
let client: ClientAPI;
@@ -14,13 +15,9 @@ export function initAPIService(
return;
}
window.refresh_token = () => {
//
};
window.refresh_token = refreshToken;
window.invalid_token = () => {
// invalidToken();
};
window.invalid_token = invalidToken;
client = ClientAPI.new({
base_url: config.baseURL,
@@ -34,16 +31,45 @@ export function initAPIService(
},
});
if (isTokenValid()) {
client.restore_token(getToken() || '');
}
client.subscribe();
}
export async function getPublishView(publishNamespace: string, publishName: string) {
const data = await client.get_publish_view(publishNamespace, publishName);
return {
data: data.data,
meta: JSON.parse(data.meta.data) as PublishViewMetaData,
};
const meta = JSON.parse(data.meta.data) as PublishViewMetaData;
if (meta.view.layout === ViewLayout.Document) {
return {
data: data.data,
meta,
};
}
try {
const decoder = new TextDecoder('utf-8');
const jsonStr = decoder.decode(new Uint8Array(data.data));
const res = JSON.parse(jsonStr) as {
database_collab: number[];
database_row_collabs: Record<string, number[]>;
database_row_document_collabs: Record<string, number[]>;
visible_database_view_ids: string[];
};
console.log('getPublishView', res);
return {
data: res.database_collab,
rows: res.database_row_collabs,
visibleViewIds: res.visible_database_view_ids,
meta,
};
} catch (e) {
return Promise.reject(e);
}
}
export async function getPublishInfoWithViewId(viewId: string) {
@@ -56,3 +82,33 @@ export async function getPublishViewMeta(publishNamespace: string, publishName:
return metadata;
}
export async function signInWithUrl(url: string) {
return client.sign_in_with_url(url);
}
export async function signInWithMagicLink(email: string, redirectTo: string) {
return client.sign_in_with_magic_link(email, redirectTo);
}
export async function signInGoogle(redirectTo: string) {
return signInProvider('google', redirectTo);
}
export async function signInProvider(provider: string, redirectTo: string) {
try {
const { url } = await client.generate_oauth_url_with_provider(provider, redirectTo);
window.open(url, '_current');
} catch (e) {
return Promise.reject(e);
}
}
export async function signInGithub(redirectTo: string) {
return signInProvider('github', redirectTo);
}
export async function signInDiscord(redirectTo: string) {
return signInProvider('discord', redirectTo);
}
@@ -15,6 +15,7 @@ export interface AFCloudConfig {
}
export interface PublishService {
getClientId: () => string;
getPublishViewMeta: (namespace: string, publishName: string) => Promise<ViewMeta>;
getPublishView: (namespace: string, publishName: string) => Promise<YDoc>;
getPublishInfo: (viewId: string) => Promise<{ namespace: string; publishName: string }>;
@@ -26,4 +27,10 @@ export interface PublishService {
rows: Y.Map<YDoc>;
destroy: () => void;
}>;
loginAuth: (url: string) => Promise<void>;
signInMagicLink: (params: { email: string; redirectTo: string }) => Promise<void>;
signInGoogle: (params: { redirectTo: string }) => Promise<void>;
signInGithub: (params: { redirectTo: string }) => Promise<void>;
signInDiscord: (params: { redirectTo: string }) => Promise<void>;
}
@@ -21,4 +21,28 @@ export class AFClientService implements AFService {
async getPublishDatabaseViewRows(_namespace: string, _publishName: string, _rowIds: string[]) {
return Promise.reject('Method not implemented');
}
getClientId(): string {
return '';
}
loginAuth(_: string): Promise<void> {
return Promise.resolve(undefined);
}
signInDiscord(_params: { redirectTo: string }): Promise<void> {
return Promise.resolve(undefined);
}
signInGithub(_params: { redirectTo: string }): Promise<void> {
return Promise.resolve(undefined);
}
signInGoogle(_params: { redirectTo: string }): Promise<void> {
return Promise.resolve(undefined);
}
signInMagicLink(_params: { email: string; redirectTo: string }): Promise<void> {
return Promise.resolve(undefined);
}
}
@@ -0,0 +1,24 @@
import { EventEmitter } from 'events';
const event = new EventEmitter();
export enum EventType {
SESSION_EXPIRED = 'session_expired',
SESSION_REFRESH = 'session_refresh',
SESSION_INVALID = 'session_invalid',
SESSION_VALID = 'session_valid',
}
export type Listener<T> = (data: T) => void;
export function on<T>(eventType: EventType, listener: Listener<T>) {
event.on(eventType, listener);
return () => {
event.off(eventType, listener);
};
}
export function emit<T>(eventType: EventType, data?: T) {
event.emit(eventType, data);
}
@@ -0,0 +1 @@
export * from './event';
@@ -0,0 +1,51 @@
export function saveRedirectTo(redirectTo: string) {
localStorage.setItem('redirectTo', redirectTo);
}
export function getRedirectTo() {
return localStorage.getItem('redirectTo');
}
export function clearRedirectTo() {
localStorage.removeItem('redirectTo');
}
export const AUTH_CALLBACK_PATH = '/auth/callback';
export const AUTH_CALLBACK_URL = `${window.location.origin}${AUTH_CALLBACK_PATH}`;
export function withSignIn() {
return function (
// eslint-disable-next-line
_target: any,
_propertyKey: string,
descriptor: PropertyDescriptor
) {
const originalMethod = descriptor.value;
// eslint-disable-next-line
descriptor.value = async function (args: { redirectTo: string }) {
const redirectTo = args.redirectTo;
saveRedirectTo(redirectTo);
console.log('=====saveRedirectTo', redirectTo);
try {
await originalMethod.apply(this, [args]);
} catch (e) {
console.error(e);
return Promise.reject(e);
}
};
return descriptor;
};
}
export function afterAuth() {
const redirectTo = getRedirectTo();
if (redirectTo) {
clearRedirectTo();
window.location.href = redirectTo;
}
}
@@ -0,0 +1,20 @@
import { emit, EventType } from '@/application/session/event';
export function refreshToken(token: string) {
localStorage.removeItem('token');
localStorage.setItem('token', token);
emit(EventType.SESSION_REFRESH, token);
}
export function invalidToken() {
localStorage.removeItem('token');
emit(EventType.SESSION_INVALID);
}
export function isTokenValid() {
return !!localStorage.getItem('token');
}
export function getToken() {
return localStorage.getItem('token');
}
@@ -0,0 +1,12 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="login">
<g id="login_2">
<path id="Vector" d="M11.6406 14.9475L14.5206 12.0675L11.6406 9.1875" stroke="currentColor"
stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_2" d="M3 12.0674H14.4413" stroke="currentColor" stroke-width="1.5" stroke-miterlimit="10"
stroke-linecap="round" stroke-linejoin="round"/>
<path id="Vector_3" d="M12 3C16.9725 3 21 6.375 21 12C21 17.625 16.9725 21 12 21" stroke="currentColor"
stroke-width="1.5" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 793 B

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="16" viewBox="0 0 20 16" fill="none">
<path d="M16.3916 2.18032C16.3865 2.17042 16.378 2.16267 16.3677 2.15844C15.1764 1.61184 13.9191 1.22207 12.6275 0.998881C12.6158 0.996699 12.6037 0.998273 12.5929 1.00338C12.5821 1.00848 12.5732 1.01686 12.5674 1.02732C12.3962 1.33803 12.2408 1.65719 12.1018 1.98357C10.7095 1.77222 9.29329 1.77222 7.901 1.98357C7.76105 1.65636 7.60315 1.33713 7.42803 1.02732C7.42202 1.01709 7.41307 1.0089 7.40235 1.00383C7.39162 0.99876 7.37962 0.997034 7.3679 0.998881C6.07615 1.2216 4.81885 1.6114 3.62765 2.15847C3.61745 2.1628 3.60885 2.17018 3.60303 2.1796C1.22087 5.73704 0.568308 9.20701 0.888433 12.634C0.889334 12.6424 0.891914 12.6505 0.896021 12.6579C0.900128 12.6653 0.905677 12.6718 0.912339 12.677C2.29945 13.704 3.85094 14.488 5.50062 14.9954C5.51224 14.9989 5.52464 14.9987 5.53617 14.9949C5.5477 14.9912 5.55779 14.9839 5.56509 14.9743C5.9194 14.4922 6.23335 13.9817 6.50375 13.4479C6.50746 13.4406 6.50958 13.4326 6.50997 13.4244C6.51036 13.4162 6.509 13.408 6.50599 13.4004C6.50298 13.3927 6.49839 13.3858 6.49251 13.3801C6.48664 13.3743 6.47961 13.3699 6.4719 13.3671C5.97683 13.1776 5.49754 12.9493 5.03853 12.6842C5.03019 12.6793 5.02319 12.6724 5.01814 12.6641C5.01309 12.6559 5.01015 12.6465 5.00958 12.6369C5.00901 12.6272 5.01082 12.6176 5.01486 12.6088C5.0189 12.6 5.02504 12.5923 5.03275 12.5865C5.12933 12.5143 5.22424 12.44 5.3174 12.3634C5.32557 12.3567 5.33546 12.3524 5.34595 12.351C5.35644 12.3496 5.36712 12.3511 5.37678 12.3554C8.38393 13.7278 11.6396 13.7278 14.6112 12.3554C14.6208 12.3508 14.6316 12.3491 14.6423 12.3504C14.6529 12.3517 14.663 12.3559 14.6713 12.3627C14.7645 12.4396 14.8597 12.5143 14.9567 12.5865C14.9644 12.5923 14.9706 12.5999 14.9747 12.6086C14.9788 12.6174 14.9807 12.627 14.9802 12.6367C14.9797 12.6463 14.9768 12.6557 14.9718 12.664C14.9668 12.6723 14.9599 12.6792 14.9516 12.6842C14.4936 12.9515 14.0139 13.1797 13.5175 13.3663C13.5098 13.3693 13.5028 13.3738 13.497 13.3796C13.4911 13.3855 13.4866 13.3925 13.4836 13.4002C13.4807 13.4079 13.4794 13.4161 13.4799 13.4243C13.4803 13.4326 13.4825 13.4406 13.4863 13.4479C13.7612 13.9787 14.0747 14.4885 14.4242 14.9734C14.4313 14.9834 14.4414 14.9908 14.4529 14.9947C14.4645 14.9987 14.477 14.9989 14.4887 14.9953C16.1413 14.4896 17.6955 13.7056 19.0844 12.677C19.0911 12.672 19.0968 12.6657 19.1009 12.6584C19.105 12.6511 19.1075 12.6431 19.1083 12.6347C19.4915 8.67276 18.4667 5.23122 16.3916 2.18032ZM6.95284 10.5473C6.04746 10.5473 5.30146 9.71647 5.30146 8.6961C5.30146 7.67572 6.033 6.84482 6.95284 6.84482C7.87987 6.84482 8.61865 7.68294 8.60418 8.69604C8.60418 9.71647 7.87262 10.5473 6.95284 10.5473ZM13.0585 10.5473C12.1531 10.5473 11.4071 9.71647 11.4071 8.6961C11.4071 7.67572 12.1387 6.84482 13.0585 6.84482C13.9856 6.84482 14.7243 7.68294 14.7098 8.69604C14.7098 9.71647 13.9856 10.5473 13.0585 10.5473Z"
fill="#5865F2"/>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
<path d="M10.0002 0.83128C7.59472 0.806836 5.27798 1.73845 3.55911 3.42138C1.84023 5.10431 0.85988 7.40085 0.833496 9.80628C0.842765 11.7073 1.45428 13.5564 2.58021 15.0881C3.70615 16.6198 5.28856 17.7552 7.10016 18.3313C7.5585 18.4146 7.72516 18.1396 7.72516 17.8979V16.3729C5.17516 16.9146 4.6335 15.1729 4.6335 15.1729C4.46374 14.6262 4.10286 14.1588 3.61683 13.8563C2.7835 13.3063 3.6835 13.3146 3.6835 13.3146C3.97154 13.3531 4.24712 13.4563 4.48959 13.6165C4.73205 13.7767 4.93509 13.9898 5.0835 14.2396C5.34211 14.688 5.76644 15.017 6.26517 15.1557C6.7639 15.2944 7.29716 15.2318 7.75016 14.9813C7.79675 14.5249 8.00341 14.0998 8.3335 13.7813C6.30016 13.5563 4.16683 12.7896 4.16683 9.34795C4.14865 8.45018 4.48036 7.58054 5.09183 6.92295C4.81335 6.15217 4.84623 5.30321 5.1835 4.55628C5.1835 4.55628 5.9585 4.31461 7.6835 5.47295C9.18507 5.07281 10.7653 5.07281 12.2668 5.47295C14.0168 4.31461 14.7668 4.55628 14.7668 4.55628C15.1041 5.30321 15.137 6.15217 14.8585 6.92295C15.4842 7.56849 15.8339 8.43229 15.8335 9.33128C15.8335 12.7813 13.6835 13.5396 11.6668 13.7646C11.8876 13.979 12.0582 14.2397 12.1663 14.5278C12.2744 14.816 12.3172 15.1246 12.2918 15.4313V17.8896C12.2918 18.1813 12.4585 18.4146 12.9168 18.3229C14.7233 17.7433 16.3004 16.6076 17.4228 15.0781C18.5453 13.5485 19.1557 11.7034 19.1668 9.80628C19.1404 7.40085 18.1601 5.10431 16.4412 3.42138C14.7223 1.73845 12.4056 0.806836 10.0002 0.83128Z"
fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,17 @@
<svg xmlns="http://www.w3.org/2000/svg" width="21" height="20" viewBox="0 0 21 20" fill="none">
<g clip-path="url(#clip0_346_13747)">
<path d="M4.68181 9.99793C4.68181 9.36293 4.79014 8.75376 4.98181 8.18293L1.61681 5.66626C0.940723 7.00999 0.589632 8.4937 0.591807 9.99793C0.591807 11.5546 0.96014 13.0229 1.61514 14.3263L4.97847 11.8054C4.78236 11.223 4.68217 10.6125 4.68181 9.99793Z"
fill="#FBBC05"/>
<path d="M10.5917 4.22046C12 4.22046 13.2725 4.70879 14.2725 5.50879L17.1817 2.66463C15.4092 1.15379 13.1367 0.220459 10.5917 0.220459C6.64003 0.220459 3.24337 2.43296 1.6167 5.66629L4.98337 8.18296C5.75837 5.87796 7.96837 4.22046 10.5917 4.22046Z"
fill="#EA4335"/>
<path d="M10.5917 15.7755C7.96753 15.7755 5.75753 14.118 4.9817 11.813L1.6167 14.3297C3.24253 17.563 6.6392 19.7755 10.5917 19.7755C13.03 19.7755 15.3584 18.928 17.1067 17.3388L13.9117 14.9205C13.0109 15.4763 11.8759 15.7755 10.5909 15.7755"
fill="#34A853"/>
<path d="M20.1367 9.99796C20.1367 9.42046 20.045 8.79796 19.9092 8.22046H10.5908V11.998H15.9542C15.6867 13.2863 14.9567 14.2763 13.9125 14.9205L17.1067 17.3388C18.9425 15.6705 20.1367 13.1855 20.1367 9.99796Z"
fill="#4285F4"/>
</g>
<defs>
<clipPath id="clip0_346_13747">
<rect width="20" height="20" fill="white" transform="translate(0.5 -0.0020752)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -1,4 +1,7 @@
import { AUTH_CALLBACK_PATH } from '@/application/session/sign_in';
import NotFound from '@/components/error/NotFound';
import LoginAuth from '@/components/login/LoginAuth';
import LoginPage from '@/pages/LoginPage';
import PublishPage from '@/pages/PublishPage';
import { BrowserRouter, Route, Routes } from 'react-router-dom';
import withAppWrapper from '@/components/app/withAppWrapper';
@@ -8,6 +11,8 @@ const AppMain = withAppWrapper(() => {
return (
<Routes>
<Route path={'/:namespace/:publishName'} element={<PublishPage />} />
<Route path={'/login'} element={<LoginPage />} />
<Route path={AUTH_CALLBACK_PATH} element={<LoginAuth />} />
<Route path='/404' element={<NotFound />} />
<Route path='*' element={<NotFound />} />
</Routes>
@@ -1,3 +1,5 @@
import { EventType, on } from '@/application/session';
import { isTokenValid } from '@/application/session/token';
import { useAppLanguage } from '@/components/app/useAppLanguage';
import { useSnackbar } from 'notistack';
import React, { createContext, useEffect, useState } from 'react';
@@ -19,6 +21,7 @@ const defaultConfig: AFServiceConfig = {
export const AFConfigContext = createContext<
| {
service: AFService | undefined;
isAuthenticated: boolean;
}
| undefined
>(undefined);
@@ -26,7 +29,29 @@ export const AFConfigContext = createContext<
function AppConfig({ children }: { children: React.ReactNode }) {
const [appConfig] = useState<AFServiceConfig>(defaultConfig);
const [service, setService] = useState<AFService>();
const [isAuthenticated, setIsAuthenticated] = React.useState<boolean>(isTokenValid());
useEffect(() => {
return on(EventType.SESSION_VALID, () => {
setIsAuthenticated(true);
});
}, []);
useEffect(() => {
const handleStorageChange = (event: StorageEvent) => {
if (event.key === 'token') setIsAuthenticated(isTokenValid());
};
window.addEventListener('storage', handleStorageChange);
return () => {
window.removeEventListener('storage', handleStorageChange);
};
}, []);
useEffect(() => {
return on(EventType.SESSION_INVALID, () => {
setIsAuthenticated(false);
});
}, []);
useAppLanguage();
useEffect(() => {
@@ -67,6 +92,7 @@ function AppConfig({ children }: { children: React.ReactNode }) {
<AFConfigContext.Provider
value={{
service,
isAuthenticated,
}}
>
{children}
@@ -1,9 +1,10 @@
import { YDatabase, YDoc, YjsDatabaseKey, YjsEditorKey } from '@/application/collab.type';
import { ViewMeta } from '@/application/db/tables/view_metas';
import ComponentLoading from '@/components/_shared/progress/ComponentLoading';
import DatabaseHeader from '@/components/database/components/header/DatabaseHeader';
import DatabaseRow from '@/components/database/DatabaseRow';
import DatabaseViews from '@/components/database/DatabaseViews';
import { ViewMetaPreview, ViewMetaProps } from '@/components/view-meta/ViewMetaPreview';
import { ViewMetaProps } from '@/components/view-meta/ViewMetaPreview';
import React, { Suspense, useCallback, useEffect, useMemo, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
import * as Y from 'yjs';
@@ -27,7 +28,7 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView,
const database = doc.getMap(YjsEditorKey.data_section).get(YjsEditorKey.database) as YDatabase;
const rows = database.get(YjsDatabaseKey.views).get(viewId).get(YjsDatabaseKey.row_orders);
return rows.toArray().map((row) => row.get(YjsDatabaseKey.id));
return rows.toJSON().map((row) => row.id);
}, [doc, viewId]);
const iidIndex = useMemo(() => {
@@ -70,7 +71,12 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView,
}
return (
<div className={'flex w-full justify-center'}>
<div
style={{
height: 'calc(100vh - 48px)',
}}
className={'flex w-full justify-center'}
>
<Suspense fallback={<ComponentLoading />}>
<DatabaseContextProvider
isDatabaseRowPage={!!rowId}
@@ -87,10 +93,15 @@ function Database({ doc, getViewRowsMap, navigateToView, loadViewMeta, loadView,
<DatabaseRow rowId={rowId} />
) : (
<div className={'relative flex h-full w-full flex-col'}>
{viewMeta && <ViewMetaPreview {...viewMeta} />}
{viewMeta && <DatabaseHeader {...viewMeta} />}
<div className='appflowy-database relative flex w-full flex-1 select-text flex-col overflow-y-hidden'>
<DatabaseViews iidIndex={iidIndex} onChangeView={handleChangeView} viewId={viewId} />
<DatabaseViews
iidIndex={iidIndex}
viewName={viewMeta.name}
onChangeView={handleChangeView}
viewId={viewId}
/>
</div>
</div>
)}
Loaded 100 of 164 files, more files were not shown because too many files have changed in this diff. Show more