Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f36e3ae378 | ||
|
|
dc6349b4b1 | ||
|
|
44d8def3ce | ||
|
|
c6ad57f11d | ||
|
|
ff23165d3e | ||
|
|
253e7597c4 | ||
|
|
d2e3fdfefd | ||
|
|
a2336f5bf7 | ||
|
|
8833df1740 | ||
|
|
1524b10a8a | ||
|
|
fe0fa9b530 | ||
|
|
80afcf44c0 | ||
|
|
e500c89978 | ||
|
|
079b9888a8 | ||
|
|
0fe383e538 | ||
|
|
c006e29afc | ||
|
|
655a2b7093 | ||
|
|
23c67bcdba | ||
|
|
521fffd97c | ||
|
|
2ecc2a67a9 | ||
|
|
2d11215bb2 |
No files matched your search
@@ -1,72 +0,0 @@
|
||||
name: Deploy Web (Test)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build/test
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.WEB_TEST_SSH_PRIVATE_KEY }}
|
||||
REMOTE_HOST: ${{ secrets.WEB_TEST_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.WEB_TEST_REMOTE_USER }}
|
||||
SSL_CERTIFICATE: ${{ secrets.WEB_TEST_SSL_CERTIFICATE }}
|
||||
SSL_CERTIFICATE_KEY: ${{ secrets.WEB_TEST_SSL_CERTIFICATE_KEY }}
|
||||
ENV_FILE: test.env
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_web_app/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm install
|
||||
- name: copy env file
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
cp ${{ env.ENV_FILE }} .env
|
||||
- name: test and lint
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run lint
|
||||
- name: build
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run build
|
||||
- name: generate SSL certificate
|
||||
run: |
|
||||
echo "${{ env.SSL_CERTIFICATE }}" > nginx-signed.crt
|
||||
echo "${{ env.SSL_CERTIFICATE_KEY }}" > nginx-signed.key
|
||||
- name: Deploy to EC2
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
with:
|
||||
SSH_PRIVATE_KEY: ${{ env.SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rlgoDzvc -i"
|
||||
SOURCE: "frontend/appflowy_web_app/dist frontend/appflowy_web_app/server.cjs frontend/appflowy_web_app/start.sh frontend/appflowy_web_app/Dockerfile frontend/appflowy_web_app/nginx.conf frontend/appflowy_web_app/.env nginx-signed.crt nginx-signed.key"
|
||||
REMOTE_HOST: ${{ env.REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ env.REMOTE_USER }}
|
||||
EXCLUDE: "frontend/appflowy_web_app/dist/, frontend/appflowy_web_app/node_modules/"
|
||||
SCRIPT_AFTER: |
|
||||
docker build -t appflowy-web-app .
|
||||
docker rm -f appflowy-web-app || true
|
||||
docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: x86_64-apple-darwin, os: macos-11, extra-build-args: "" }
|
||||
- { target: x86_64-apple-darwin, os: macos-12, extra-build-args: "" }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
|
||||
os: macos-11,
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
|
||||
@@ -1,4 +1,16 @@
|
||||
# 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
|
||||
|
||||
## Version 0.6.2 - 01/07/2024
|
||||
### New Features
|
||||
- Added support for duplicating spaces.
|
||||
|
||||
Vendored
+1
-1
@@ -138,4 +138,4 @@
|
||||
"cwd": "${workspaceRoot}/appflowy_tauri/"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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.2"
|
||||
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
|
||||
|
||||
@@ -8,9 +8,10 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Empty', () {
|
||||
testWidgets('toggle theme mode', (tester) async {
|
||||
testWidgets('empty test', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.wait(1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
+4
-3
@@ -51,13 +51,14 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
final shareButton = find.byType(ShareActionList);
|
||||
final shareButton = find.byType(DocumentShareButton);
|
||||
final shareButtonState =
|
||||
tester.state(shareButton) as ShareActionListState;
|
||||
tester.widget(shareButton) as DocumentShareButton;
|
||||
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(
|
||||
context.applicationDataDirectory,
|
||||
'${shareButtonState.name}.md',
|
||||
'${shareButtonState.view.name}.md',
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
+1
-2
@@ -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.
|
||||
|
||||
+1
-1
@@ -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;
|
||||
}
|
||||
|
||||
+2
-2
@@ -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';
|
||||
@@ -119,7 +120,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
final uniqueMessages = {...allMessages, ...messages}.toList()
|
||||
..sort((a, b) => b.id.compareTo(a.id));
|
||||
uniqueMessages.insertAll(0, onetimeMessages);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: uniqueMessages,
|
||||
@@ -380,7 +380,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
}
|
||||
|
||||
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
|
||||
final streamMessageId = nanoid();
|
||||
final streamMessageId = (questionMessageId + 1).toString();
|
||||
|
||||
lastStreamMessageId = streamMessageId;
|
||||
|
||||
return TextMessage(
|
||||
@@ -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';
|
||||
|
||||
|
||||
+373
@@ -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
-242
@@ -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 {
|
||||
|
||||
-1
@@ -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
-1
@@ -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';
|
||||
|
||||
+2
-2
@@ -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');
|
||||
|
||||
+8
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+186
-28
@@ -1,58 +1,190 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'document_share_bloc.freezed.dart';
|
||||
|
||||
const _url = 'https://appflowy.com';
|
||||
|
||||
class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
|
||||
DocumentShareBloc({
|
||||
required this.view,
|
||||
}) : super(const DocumentShareState.initial()) {
|
||||
}) : super(DocumentShareState.initial()) {
|
||||
on<DocumentShareEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
viewListener = ViewListener(viewId: view.id)
|
||||
..start(
|
||||
onViewUpdated: (value) {
|
||||
add(DocumentShareEvent.updateViewName(value.name));
|
||||
},
|
||||
onViewMoveToTrash: (p0) {
|
||||
add(const DocumentShareEvent.setPublishStatus(false));
|
||||
},
|
||||
);
|
||||
|
||||
add(const DocumentShareEvent.updatePublishStatus());
|
||||
},
|
||||
share: (type, path) async {
|
||||
if (DocumentShareType.unimplemented.contains(type)) {
|
||||
Log.error('DocumentShareType $type is not implemented');
|
||||
return;
|
||||
}
|
||||
|
||||
emit(const DocumentShareState.loading());
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
final exporter = DocumentExporter(view);
|
||||
final FlowyResult<ExportDataPB, FlowyError> result =
|
||||
await exporter.export(type.exportType).then((value) {
|
||||
return value.fold(
|
||||
(s) {
|
||||
if (path != null) {
|
||||
switch (type) {
|
||||
case DocumentShareType.markdown:
|
||||
return FlowyResult.success(_saveMarkdownToPath(s, path));
|
||||
case DocumentShareType.html:
|
||||
return FlowyResult.success(_saveHTMLToPath(s, path));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return FlowyResult.failure(FlowyError());
|
||||
},
|
||||
(f) => FlowyResult.failure(f),
|
||||
final result = await _export(type, path);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
exportResult: result,
|
||||
),
|
||||
);
|
||||
},
|
||||
publish: (nameSpace, publishName) async {
|
||||
// set space name
|
||||
try {
|
||||
final result =
|
||||
await ViewBackendService.getPublishNameSpace().getOrThrow();
|
||||
|
||||
await ViewBackendService.publish(
|
||||
view,
|
||||
name: publishName,
|
||||
).getOrThrow();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: true,
|
||||
publishResult: FlowySuccess(null),
|
||||
unpublishResult: null,
|
||||
url: '$_url/${result.namespace}/$publishName',
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('publish error: $e');
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: false,
|
||||
publishResult: FlowyResult.failure(
|
||||
FlowyError(msg: 'publish error: $e'),
|
||||
),
|
||||
unpublishResult: null,
|
||||
url: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
unPublish: () async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
publishResult: null,
|
||||
unpublishResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
final result = await ViewBackendService.unpublish(view);
|
||||
final isPublished = !result.isSuccess;
|
||||
result.onFailure((f) {
|
||||
Log.error('unpublish error: $f');
|
||||
});
|
||||
|
||||
emit(DocumentShareState.finish(result));
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: isPublished,
|
||||
publishResult: null,
|
||||
unpublishResult: result,
|
||||
url: result.fold((_) => '', (_) => state.url),
|
||||
),
|
||||
);
|
||||
},
|
||||
updateViewName: (viewName) async {
|
||||
emit(state.copyWith(viewName: viewName));
|
||||
},
|
||||
setPublishStatus: (isPublished) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: isPublished,
|
||||
url: isPublished ? state.url : '',
|
||||
),
|
||||
);
|
||||
},
|
||||
updatePublishStatus: () async {
|
||||
final publishInfo = await ViewBackendService.getPublishInfo(view);
|
||||
final enablePublish =
|
||||
await UserBackendService.getCurrentUserProfile().fold(
|
||||
(v) => v.authenticator == AuthenticatorPB.AppFlowyCloud,
|
||||
(p) => false,
|
||||
);
|
||||
publishInfo.fold((s) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: true,
|
||||
url: '$_url/${s.namespace}/${s.publishName}',
|
||||
viewName: view.name,
|
||||
enablePublish: enablePublish,
|
||||
),
|
||||
);
|
||||
}, (f) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: false,
|
||||
url: '',
|
||||
viewName: view.name,
|
||||
enablePublish: enablePublish,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
late final ViewListener viewListener;
|
||||
|
||||
late final exporter = DocumentExporter(view);
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await viewListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<FlowyResult<ExportDataPB, FlowyError>> _export(
|
||||
DocumentShareType type,
|
||||
String? path,
|
||||
) async {
|
||||
final result = await exporter.export(type.exportType);
|
||||
return result.fold(
|
||||
(s) {
|
||||
if (path != null) {
|
||||
switch (type) {
|
||||
case DocumentShareType.markdown:
|
||||
return FlowySuccess(_saveMarkdownToPath(s, path));
|
||||
case DocumentShareType.html:
|
||||
return FlowySuccess(_saveHTMLToPath(s, path));
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return FlowyResult.failure(FlowyError());
|
||||
},
|
||||
(f) => FlowyResult.failure(f),
|
||||
);
|
||||
}
|
||||
|
||||
ExportDataPB _saveMarkdownToPath(String markdown, String path) {
|
||||
File(path).writeAsStringSync(markdown);
|
||||
@@ -93,15 +225,41 @@ enum DocumentShareType {
|
||||
|
||||
@freezed
|
||||
class DocumentShareEvent with _$DocumentShareEvent {
|
||||
const factory DocumentShareEvent.share(DocumentShareType type, String? path) =
|
||||
Share;
|
||||
const factory DocumentShareEvent.initial() = _Initial;
|
||||
const factory DocumentShareEvent.share(
|
||||
DocumentShareType type,
|
||||
String? path,
|
||||
) = _Share;
|
||||
const factory DocumentShareEvent.publish(
|
||||
String nameSpace,
|
||||
String pageId,
|
||||
) = _Publish;
|
||||
const factory DocumentShareEvent.unPublish() = _UnPublish;
|
||||
const factory DocumentShareEvent.updateViewName(String name) =
|
||||
_UpdateViewName;
|
||||
const factory DocumentShareEvent.updatePublishStatus() = _UpdatePublishStatus;
|
||||
const factory DocumentShareEvent.setPublishStatus(bool isPublished) =
|
||||
_SetPublishStatus;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentShareState with _$DocumentShareState {
|
||||
const factory DocumentShareState.initial() = _Initial;
|
||||
const factory DocumentShareState.loading() = _Loading;
|
||||
const factory DocumentShareState.finish(
|
||||
FlowyResult<ExportDataPB, FlowyError> successOrFail,
|
||||
) = _Finish;
|
||||
const factory DocumentShareState({
|
||||
required bool isPublished,
|
||||
required bool isLoading,
|
||||
required String url,
|
||||
required String viewName,
|
||||
required bool enablePublish,
|
||||
FlowyResult<ExportDataPB, FlowyError>? exportResult,
|
||||
FlowyResult<void, FlowyError>? publishResult,
|
||||
FlowyResult<void, FlowyError>? unpublishResult,
|
||||
}) = _DocumentShareState;
|
||||
|
||||
factory DocumentShareState.initial() => const DocumentShareState(
|
||||
isLoading: false,
|
||||
isPublished: false,
|
||||
enablePublish: true,
|
||||
url: '',
|
||||
viewName: '',
|
||||
);
|
||||
}
|
||||
+23
-13
@@ -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(
|
||||
|
||||
+16
-5
@@ -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(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
+13
-3
@@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+21
-5
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart';
|
||||
@@ -9,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -284,6 +286,14 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
|
||||
if (canUpdateCachedKeyboardHeight) {
|
||||
cachedKeyboardHeight.value = height;
|
||||
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
// cache the keyboard height with the view padding in Android
|
||||
if (cachedKeyboardHeight.value != 0) {
|
||||
cachedKeyboardHeight.value +=
|
||||
MediaQuery.of(context).viewPadding.bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (height == 0) {
|
||||
@@ -385,13 +395,19 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: showMenuNotifier,
|
||||
builder: (_, showingMenu, __) {
|
||||
var paddingHeight = height;
|
||||
if (Platform.isAndroid) {
|
||||
paddingHeight =
|
||||
height + MediaQuery.of(context).viewPadding.bottom;
|
||||
var keyboardHeight = height;
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
if (!showingMenu) {
|
||||
// take the max value of the keyboard height and the view padding
|
||||
// to make sure the toolbar is above the keyboard
|
||||
keyboardHeight = max(
|
||||
keyboardHeight,
|
||||
MediaQuery.of(context).viewInsets.bottom,
|
||||
);
|
||||
}
|
||||
}
|
||||
return SizedBox(
|
||||
height: paddingHeight,
|
||||
height: keyboardHeight,
|
||||
child: (showingMenu && selectedMenuIndex != null)
|
||||
? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(
|
||||
context,
|
||||
|
||||
+50
@@ -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
@@ -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';
|
||||
@@ -0,0 +1,130 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
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';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ExportTab extends StatelessWidget {
|
||||
const ExportTab({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const VSpace(10),
|
||||
_ExportButton(
|
||||
title: LocaleKeys.shareAction_html.tr(),
|
||||
svg: FlowySvgs.export_html_s,
|
||||
onTap: () => _exportHTML(context),
|
||||
),
|
||||
const VSpace(10),
|
||||
_ExportButton(
|
||||
title: LocaleKeys.shareAction_markdown.tr(),
|
||||
svg: FlowySvgs.export_markdown_s,
|
||||
onTap: () => _exportMarkdown(context),
|
||||
),
|
||||
const VSpace(10),
|
||||
_ExportButton(
|
||||
title: LocaleKeys.shareAction_clipboard.tr(),
|
||||
svg: FlowySvgs.duplicate_s,
|
||||
onTap: () => _exportToClipboard(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportHTML(BuildContext context) async {
|
||||
final viewName = context.read<DocumentShareBloc>().state.viewName;
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${viewName.toFileName()}.html',
|
||||
);
|
||||
if (context.mounted && exportPath != null) {
|
||||
context.read<DocumentShareBloc>().add(
|
||||
DocumentShareEvent.share(
|
||||
DocumentShareType.html,
|
||||
exportPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportMarkdown(BuildContext context) async {
|
||||
final viewName = context.read<DocumentShareBloc>().state.viewName;
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${viewName.toFileName()}.md',
|
||||
);
|
||||
if (context.mounted && exportPath != null) {
|
||||
context.read<DocumentShareBloc>().add(
|
||||
DocumentShareEvent.share(
|
||||
DocumentShareType.markdown,
|
||||
exportPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportToClipboard(BuildContext context) async {
|
||||
final documentExporter =
|
||||
DocumentExporter(context.read<DocumentShareBloc>().view);
|
||||
final result = await documentExporter.export(DocumentExportType.markdown);
|
||||
result.fold(
|
||||
(markdown) {
|
||||
getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(plainText: markdown),
|
||||
);
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copiedNotification.tr(),
|
||||
);
|
||||
},
|
||||
(error) => showToastNotification(context, message: error.msg),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExportButton extends StatelessWidget {
|
||||
const _ExportButton({
|
||||
required this.title,
|
||||
required this.svg,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final FlowySvgData svg;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).isLightMode
|
||||
? const Color(0x1E14171B)
|
||||
: Colors.white.withOpacity(0.1);
|
||||
final radius = BorderRadius.circular(10.0);
|
||||
return FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||
iconPadding: 12,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: color,
|
||||
),
|
||||
borderRadius: radius,
|
||||
),
|
||||
radius: radius,
|
||||
text: FlowyText(title),
|
||||
leftIcon: FlowySvg(svg),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ShareMenuColors {
|
||||
static Color borderColor(BuildContext context) {
|
||||
final borderColor = Theme.of(context).isLightMode
|
||||
? const Color(0x1E14171B)
|
||||
: Colors.white.withOpacity(0.1);
|
||||
return borderColor;
|
||||
}
|
||||
}
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
String replaceInvalidChars(String input) {
|
||||
final RegExp invalidCharsRegex = RegExp('[^a-zA-Z0-9-]');
|
||||
return input.replaceAll(invalidCharsRegex, '-');
|
||||
}
|
||||
|
||||
Future<String> generateNameSpace() async {
|
||||
return '';
|
||||
}
|
||||
|
||||
// The backend limits the publish name to a maximum of 120 characters.
|
||||
// If the combined length of the ID and the name exceeds 120 characters,
|
||||
// we will truncate the name to ensure the final result is within the limit.
|
||||
// The name should only contain alphanumeric characters and hyphens.
|
||||
Future<String> generatePublishName(String id, String name) async {
|
||||
if (name.length >= 120 - id.length) {
|
||||
name = name.substring(0, 120 - id.length);
|
||||
}
|
||||
return replaceInvalidChars('$name-$id');
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/pubish_color_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/publish_name_generator.dart';
|
||||
import 'package:appflowy/startup/startup.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_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class PublishTab extends StatelessWidget {
|
||||
const PublishTab({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocConsumer<DocumentShareBloc, DocumentShareState>(
|
||||
listener: (context, state) {
|
||||
_showToast(context, state);
|
||||
},
|
||||
builder: (context, state) {
|
||||
return state.isPublished
|
||||
? _PublishedWidget(
|
||||
url: state.url,
|
||||
onVisitSite: () {},
|
||||
onUnPublish: () {
|
||||
context
|
||||
.read<DocumentShareBloc>()
|
||||
.add(const DocumentShareEvent.unPublish());
|
||||
},
|
||||
)
|
||||
: _UnPublishWidget(
|
||||
onPublish: () async {
|
||||
final id = context.read<DocumentShareBloc>().view.id;
|
||||
final publishName = await generatePublishName(
|
||||
id,
|
||||
state.viewName,
|
||||
);
|
||||
if (context.mounted) {
|
||||
context.read<DocumentShareBloc>().add(
|
||||
DocumentShareEvent.publish('', publishName),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showToast(BuildContext context, DocumentShareState state) {
|
||||
if (state.publishResult != null) {
|
||||
state.publishResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_publishSuccessfully.tr(),
|
||||
),
|
||||
(error) => showToastNotification(
|
||||
context,
|
||||
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
|
||||
),
|
||||
);
|
||||
} else if (state.unpublishResult != null) {
|
||||
state.unpublishResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||
),
|
||||
(error) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishFailed.tr(),
|
||||
description: error.msg,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishedWidget extends StatefulWidget {
|
||||
const _PublishedWidget({
|
||||
required this.url,
|
||||
required this.onVisitSite,
|
||||
required this.onUnPublish,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final VoidCallback onVisitSite;
|
||||
final VoidCallback onUnPublish;
|
||||
|
||||
@override
|
||||
State<_PublishedWidget> createState() => _PublishedWidgetState();
|
||||
}
|
||||
|
||||
class _PublishedWidgetState extends State<_PublishedWidget> {
|
||||
final controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.text = widget.url;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(16),
|
||||
const _PublishTabHeader(),
|
||||
const VSpace(16),
|
||||
_PublishUrl(
|
||||
controller: controller,
|
||||
onCopy: (url) {
|
||||
getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(plainText: url),
|
||||
);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copy.tr(),
|
||||
);
|
||||
},
|
||||
onSubmitted: (url) {},
|
||||
),
|
||||
const VSpace(16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildUnpublishButton(),
|
||||
const Spacer(),
|
||||
_buildVisitSiteButton(),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnpublishButton() {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
width: 184,
|
||||
child: FlowyButton(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: ShareMenuColors.borderColor(context)),
|
||||
),
|
||||
radius: BorderRadius.circular(10),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.shareAction_unPublish.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
onTap: widget.onUnPublish,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVisitSiteButton() {
|
||||
return RoundedTextButton(
|
||||
onPressed: () {
|
||||
safeLaunchUrl(controller.text);
|
||||
},
|
||||
title: LocaleKeys.shareAction_visitSite.tr(),
|
||||
width: 184,
|
||||
height: 36,
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UnPublishWidget extends StatelessWidget {
|
||||
const _UnPublishWidget({
|
||||
required this.onPublish,
|
||||
});
|
||||
|
||||
final VoidCallback onPublish;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(16),
|
||||
const _PublishTabHeader(),
|
||||
const VSpace(16),
|
||||
RoundedTextButton(
|
||||
height: 36,
|
||||
title: LocaleKeys.shareAction_publish.tr(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 9.0),
|
||||
fontSize: 14.0,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: onPublish,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishTabHeader extends StatelessWidget {
|
||||
const _PublishTabHeader();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const FlowySvg(FlowySvgs.share_publish_s),
|
||||
const HSpace(6),
|
||||
FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()),
|
||||
],
|
||||
),
|
||||
const VSpace(4),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.shareAction_publishToTheWebHint.tr(),
|
||||
fontSize: 12,
|
||||
maxLines: 3,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishUrl extends StatelessWidget {
|
||||
const _PublishUrl({
|
||||
required this.controller,
|
||||
required this.onCopy,
|
||||
required this.onSubmitted,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final void Function(String url) onCopy;
|
||||
final void Function(String url) onSubmitted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: FlowyTextField(
|
||||
readOnly: true,
|
||||
autoFocus: false,
|
||||
controller: controller,
|
||||
enableBorderColor: ShareMenuColors.borderColor(context),
|
||||
suffixIcon: _buildCopyLinkIcon(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCopyLinkIcon(BuildContext context) {
|
||||
return FlowyHover(
|
||||
child: GestureDetector(
|
||||
onTap: () => onCopy(controller.text),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(left: BorderSide(color: Color(0x141F2329))),
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.m_toolbar_link_m,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+54
-13
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_menu.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
@@ -13,6 +14,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
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';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -28,23 +30,44 @@ class DocumentShareButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DocumentShareBloc>(param1: view),
|
||||
create: (context) => getIt<DocumentShareBloc>(param1: view)
|
||||
..add(const DocumentShareEvent.initial()),
|
||||
child: BlocListener<DocumentShareBloc, DocumentShareState>(
|
||||
listener: (context, state) {
|
||||
state.mapOrNull(
|
||||
finish: (state) {
|
||||
state.successOrFail.fold(
|
||||
(data) => _handleExportData(context, data),
|
||||
_handleExportError,
|
||||
);
|
||||
},
|
||||
);
|
||||
if (state.isLoading == false && state.exportResult != null) {
|
||||
state.exportResult!.fold(
|
||||
(data) => _handleExportData(context, data),
|
||||
_handleExportError,
|
||||
);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
|
||||
builder: (context, state) => SizedBox(
|
||||
height: 32.0,
|
||||
child: IntrinsicWidth(child: ShareActionList(view: view)),
|
||||
),
|
||||
builder: (context, state) {
|
||||
final tabs = [
|
||||
if (state.enablePublish) ShareMenuTab.publish,
|
||||
ShareMenuTab.exportAs,
|
||||
];
|
||||
final shareBloc = context.read<DocumentShareBloc>();
|
||||
return SizedBox(
|
||||
height: 32.0,
|
||||
child: IntrinsicWidth(
|
||||
child: AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 422,
|
||||
),
|
||||
offset: const Offset(0, 8),
|
||||
popupBuilder: (context) => BlocProvider.value(
|
||||
value: shareBloc,
|
||||
child: ShareMenu(
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
child: const InnerDocumentShareButton(),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -75,6 +98,24 @@ class DocumentShareButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class InnerDocumentShareButton extends StatelessWidget {
|
||||
const InnerDocumentShareButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14.0),
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10.0),
|
||||
),
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareActionList extends StatefulWidget {
|
||||
const ShareActionList({
|
||||
super.key,
|
||||
|
||||
@@ -0,0 +1,186 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/export_tab.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'publish_tab.dart';
|
||||
|
||||
enum ShareMenuTab {
|
||||
share,
|
||||
publish,
|
||||
exportAs;
|
||||
|
||||
String get i18n {
|
||||
switch (this) {
|
||||
case ShareMenuTab.share:
|
||||
return LocaleKeys.shareAction_shareTab.tr();
|
||||
case ShareMenuTab.publish:
|
||||
return LocaleKeys.shareAction_publishTab.tr();
|
||||
case ShareMenuTab.exportAs:
|
||||
return LocaleKeys.shareAction_exportAsTab.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShareMenu extends StatefulWidget {
|
||||
const ShareMenu({
|
||||
super.key,
|
||||
required this.tabs,
|
||||
});
|
||||
|
||||
final List<ShareMenuTab> tabs;
|
||||
|
||||
@override
|
||||
State<ShareMenu> createState() => _ShareMenuState();
|
||||
}
|
||||
|
||||
class _ShareMenuState extends State<ShareMenu>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late ShareMenuTab selectedTab = widget.tabs.first;
|
||||
late final tabController = TabController(
|
||||
length: widget.tabs.length,
|
||||
vsync: this,
|
||||
initialIndex: widget.tabs.indexOf(selectedTab),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.tabs.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const VSpace(10),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 30,
|
||||
child: _buildTabBar(context),
|
||||
),
|
||||
Divider(
|
||||
color: Theme.of(context).dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14.0),
|
||||
child: _buildTab(context),
|
||||
),
|
||||
const VSpace(20),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildTabBar(BuildContext context) {
|
||||
final children = [
|
||||
for (final tab in widget.tabs)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: _Segment(
|
||||
tab: tab,
|
||||
isSelected: selectedTab == tab,
|
||||
),
|
||||
),
|
||||
];
|
||||
return TabBar(
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicator: RoundUnderlineTabIndicator(
|
||||
width: 68.0,
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 3,
|
||||
),
|
||||
insets: const EdgeInsets.only(bottom: -2),
|
||||
),
|
||||
isScrollable: true,
|
||||
controller: tabController,
|
||||
tabs: children,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
selectedTab = widget.tabs[index];
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(BuildContext context) {
|
||||
switch (selectedTab) {
|
||||
case ShareMenuTab.publish:
|
||||
return const PublishTab();
|
||||
case ShareMenuTab.exportAs:
|
||||
return const ExportTab();
|
||||
default:
|
||||
return const Center(
|
||||
child: FlowyText('🏡 under construction'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Segment extends StatefulWidget {
|
||||
const _Segment({
|
||||
required this.tab,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final ShareMenuTab tab;
|
||||
|
||||
@override
|
||||
State<_Segment> createState() => _SegmentState();
|
||||
}
|
||||
|
||||
class _SegmentState extends State<_Segment> {
|
||||
bool isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color? textColor = Theme.of(context).hintColor;
|
||||
if (isHovered) {
|
||||
textColor = const Color(0xFF00BCF0);
|
||||
} else if (widget.isSelected) {
|
||||
textColor = null;
|
||||
}
|
||||
|
||||
Widget child = MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
child: FlowyText(
|
||||
widget.tab.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
color: textColor,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.tab == ShareMenuTab.publish) {
|
||||
final isPublished = context.watch<DocumentShareBloc>().state.isPublished;
|
||||
// show checkmark icon if published
|
||||
if (isPublished) {
|
||||
child = Row(
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.published_checkmark_s,
|
||||
blendMode: null,
|
||||
),
|
||||
const HSpace(6),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -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;
|
||||
|
||||
+2
-10
@@ -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),
|
||||
|
||||
+1
-10
@@ -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),
|
||||
|
||||
+11
-10
@@ -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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -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';
|
||||
@@ -5,6 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
|
||||
show WorkspaceSettingPB;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'home_bloc.freezed.dart';
|
||||
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
|
||||
@@ -54,9 +54,11 @@ class SidebarSectionsBloc
|
||||
_initial(userProfile, workspaceId);
|
||||
final sectionViews = await _getSectionViews();
|
||||
if (sectionViews != null) {
|
||||
final containsSpace = _containsSpace(sectionViews);
|
||||
emit(
|
||||
state.copyWith(
|
||||
section: sectionViews,
|
||||
containsSpace: containsSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -65,9 +67,11 @@ class SidebarSectionsBloc
|
||||
_reset(userProfile, workspaceId);
|
||||
final sectionViews = await _getSectionViews();
|
||||
if (sectionViews != null) {
|
||||
final containsSpace = _containsSpace(sectionViews);
|
||||
emit(
|
||||
state.copyWith(
|
||||
section: sectionViews,
|
||||
containsSpace: containsSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -160,9 +164,11 @@ class SidebarSectionsBloc
|
||||
_initial(userProfile, workspaceId);
|
||||
final sectionViews = await _getSectionViews();
|
||||
if (sectionViews != null) {
|
||||
final containsSpace = _containsSpace(sectionViews);
|
||||
emit(
|
||||
state.copyWith(
|
||||
section: sectionViews,
|
||||
containsSpace: containsSpace,
|
||||
),
|
||||
);
|
||||
// try to open the fist view in public section or private section
|
||||
@@ -229,6 +235,11 @@ class SidebarSectionsBloc
|
||||
}
|
||||
}
|
||||
|
||||
bool _containsSpace(SidebarSection section) {
|
||||
return section.publicViews.any((view) => view.isSpace) ||
|
||||
section.privateViews.any((view) => view.isSpace);
|
||||
}
|
||||
|
||||
void _initial(UserProfilePB userProfile, String workspaceId) {
|
||||
_workspaceService = WorkspaceService(workspaceId: workspaceId);
|
||||
|
||||
@@ -292,6 +303,7 @@ class SidebarSectionsState with _$SidebarSectionsState {
|
||||
required SidebarSection section,
|
||||
@Default(null) ViewPB? lastCreatedRootView,
|
||||
FlowyResult<void, FlowyError>? createRootViewResult,
|
||||
@Default(true) bool containsSpace,
|
||||
}) = _SidebarSectionsState;
|
||||
|
||||
factory SidebarSectionsState.initial() => const SidebarSectionsState(
|
||||
|
||||
+6
-3
@@ -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),
|
||||
(_) => [],
|
||||
);
|
||||
}
|
||||
|
||||
+140
@@ -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;
|
||||
}
|
||||
+54
@@ -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;
|
||||
}
|
||||
}
|
||||
+88
@@ -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;
|
||||
}
|
||||
-195
@@ -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();
|
||||
}
|
||||
@@ -4,12 +4,14 @@ import 'dart:convert';
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/list_extension.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/workspace_sections_listener.dart';
|
||||
import 'package:appflowy/workspace/application/workspace/workspace_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@@ -20,6 +22,7 @@ import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:protobuf/protobuf.dart';
|
||||
@@ -68,6 +71,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
_initial(userProfile, workspaceId);
|
||||
|
||||
final (spaces, publicViews, privateViews) = await _getSpaces();
|
||||
|
||||
final shouldShowUpgradeDialog = await this.shouldShowUpgradeDialog(
|
||||
spaces: spaces,
|
||||
publicViews: publicViews,
|
||||
@@ -82,13 +86,12 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
currentSpace: currentSpace,
|
||||
isExpanded: isExpanded,
|
||||
shouldShowUpgradeDialog: shouldShowUpgradeDialog,
|
||||
isInitialized: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldShowUpgradeDialog) {
|
||||
if (!integrationMode().isTest) {
|
||||
add(const SpaceEvent.migrate());
|
||||
}
|
||||
if (shouldShowUpgradeDialog && !integrationMode().isTest) {
|
||||
add(const SpaceEvent.migrate());
|
||||
}
|
||||
|
||||
if (openFirstPage) {
|
||||
@@ -138,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));
|
||||
@@ -192,13 +195,31 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
open: (space) async {
|
||||
await _openSpace(space);
|
||||
final isExpanded = await _getSpaceExpandStatus(space);
|
||||
emit(state.copyWith(currentSpace: space, isExpanded: isExpanded));
|
||||
final views = await ViewBackendService.getChildViews(
|
||||
viewId: space.id,
|
||||
);
|
||||
final currentSpace = views.fold(
|
||||
(views) {
|
||||
space.freeze();
|
||||
return space.rebuild((b) {
|
||||
b.childViews.clear();
|
||||
b.childViews.addAll(views);
|
||||
});
|
||||
},
|
||||
(_) => space,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentSpace: currentSpace,
|
||||
isExpanded: isExpanded,
|
||||
),
|
||||
);
|
||||
|
||||
// don't open the page automatically on mobile
|
||||
if (PlatformExtension.isDesktop) {
|
||||
// open the first page by default
|
||||
if (space.childViews.isNotEmpty) {
|
||||
final firstPage = space.childViews.first;
|
||||
if (currentSpace.childViews.isNotEmpty) {
|
||||
final firstPage = currentSpace.childViews.first;
|
||||
emit(
|
||||
state.copyWith(
|
||||
lastCreatedPage: firstPage,
|
||||
@@ -330,8 +351,9 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
if (sectionViews == null || sectionViews.views.isEmpty) {
|
||||
return (<ViewPB>[], <ViewPB>[], <ViewPB>[]);
|
||||
}
|
||||
final publicViews = sectionViews.publicViews;
|
||||
final privateViews = sectionViews.privateViews;
|
||||
|
||||
final publicViews = sectionViews.publicViews.unique((e) => e.id);
|
||||
final privateViews = sectionViews.privateViews.unique((e) => e.id);
|
||||
|
||||
final publicSpaces = publicViews.where((e) => e.isSpace);
|
||||
final privateSpaces = privateViews.where((e) => e.isSpace);
|
||||
@@ -411,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());
|
||||
},
|
||||
);
|
||||
@@ -481,6 +504,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
if (_workspaceId == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final user =
|
||||
await UserBackendService.getCurrentUserProfile().getOrThrow();
|
||||
@@ -504,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) {
|
||||
@@ -546,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;
|
||||
}
|
||||
@@ -690,6 +728,7 @@ class SpaceState with _$SpaceState {
|
||||
FlowyResult<void, FlowyError>? createPageResult,
|
||||
@Default(false) bool shouldShowUpgradeDialog,
|
||||
@Default(false) bool isDuplicatingSpace,
|
||||
@Default(false) bool isInitialized,
|
||||
}) = _SpaceState;
|
||||
|
||||
factory SpaceState.initial() => const SpaceState();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_listener.dart';
|
||||
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.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';
|
||||
@@ -138,11 +140,18 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
);
|
||||
},
|
||||
delete: (e) async {
|
||||
final result = await ViewBackendService.delete(viewId: view.id);
|
||||
// unpublish the page and all its child pages if they are published
|
||||
await _unpublishPage(view);
|
||||
|
||||
final result = await ViewBackendService.deleteView(viewId: view.id);
|
||||
|
||||
emit(
|
||||
result.fold(
|
||||
(l) =>
|
||||
state.copyWith(successOrFailure: FlowyResult.success(null)),
|
||||
(l) {
|
||||
return state.copyWith(
|
||||
successOrFailure: FlowyResult.success(null),
|
||||
);
|
||||
},
|
||||
(error) => state.copyWith(
|
||||
successOrFailure: FlowyResult.failure(error),
|
||||
),
|
||||
@@ -180,8 +189,11 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
);
|
||||
emit(
|
||||
result.fold(
|
||||
(l) =>
|
||||
state.copyWith(successOrFailure: FlowyResult.success(null)),
|
||||
(l) {
|
||||
return state.copyWith(
|
||||
successOrFailure: FlowyResult.success(null),
|
||||
);
|
||||
},
|
||||
(error) => state.copyWith(
|
||||
successOrFailure: FlowyResult.failure(error),
|
||||
),
|
||||
@@ -236,6 +248,13 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
}
|
||||
add(const ViewEvent.setIsExpanded(false));
|
||||
},
|
||||
unpublish: (value) async {
|
||||
if (value.sync) {
|
||||
await _unpublishPage(view);
|
||||
} else {
|
||||
unawaited(_unpublishPage(view));
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -383,6 +402,20 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
return null;
|
||||
}
|
||||
|
||||
// unpublish the page and all its child pages
|
||||
Future<void> _unpublishPage(ViewPB views) async {
|
||||
final (_, publishedPages) = await ViewBackendService.containPublishedPage(
|
||||
view,
|
||||
);
|
||||
|
||||
await Future.wait(
|
||||
publishedPages.map((view) async {
|
||||
Log.info('unpublishing page: ${view.id}, ${view.name}');
|
||||
await ViewBackendService.unpublish(view);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
bool _isSameViewIgnoreChildren(ViewPB from, ViewPB to) {
|
||||
return _hash(from) == _hash(to);
|
||||
}
|
||||
@@ -430,6 +463,8 @@ class ViewEvent with _$ViewEvent {
|
||||
) = UpdateViewVisibility;
|
||||
const factory ViewEvent.updateIcon(String? icon) = UpdateIcon;
|
||||
const factory ViewEvent.collapseAllPages() = CollapseAllPages;
|
||||
// this event will unpublish the page and all its child pages if they are published
|
||||
const factory ViewEvent.unpublish({required bool sync}) = Unpublish;
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -291,4 +291,78 @@ class ViewBackendService {
|
||||
);
|
||||
return FolderEventUpdateViewVisibilityStatus(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<PublishInfoResponsePB, FlowyError>> getPublishInfo(
|
||||
ViewPB view,
|
||||
) async {
|
||||
final payload = ViewIdPB()..value = view.id;
|
||||
return FolderEventGetPublishInfo(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> publish(
|
||||
ViewPB view, {
|
||||
String? name,
|
||||
}) async {
|
||||
final payload = PublishViewParamsPB()..viewId = view.id;
|
||||
|
||||
if (name != null) {
|
||||
payload.publishName = name;
|
||||
}
|
||||
return FolderEventPublishView(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> unpublish(
|
||||
ViewPB view,
|
||||
) async {
|
||||
final payload = UnpublishViewsPayloadPB(viewIds: [view.id]);
|
||||
return FolderEventUnpublishViews(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> setPublishNameSpace(
|
||||
String name,
|
||||
) async {
|
||||
final payload = SetPublishNamespacePayloadPB()..newNamespace = name;
|
||||
return FolderEventSetPublishNamespace(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<PublishNamespacePB, FlowyError>>
|
||||
getPublishNameSpace() async {
|
||||
return FolderEventGetPublishNamespace().send();
|
||||
}
|
||||
|
||||
static Future<List<ViewPB>> getAllChildViews(ViewPB view) async {
|
||||
final views = <ViewPB>[];
|
||||
|
||||
final childViews =
|
||||
await ViewBackendService.getChildViews(viewId: view.id).fold(
|
||||
(s) => s,
|
||||
(f) => [],
|
||||
);
|
||||
|
||||
for (final child in childViews) {
|
||||
// filter the view itself
|
||||
if (child.id == view.id) {
|
||||
continue;
|
||||
}
|
||||
views.add(child);
|
||||
views.addAll(await getAllChildViews(child));
|
||||
}
|
||||
|
||||
return views;
|
||||
}
|
||||
|
||||
static Future<(bool, List<ViewPB>)> containPublishedPage(ViewPB view) async {
|
||||
final childViews = await ViewBackendService.getAllChildViews(view);
|
||||
final views = [view, ...childViews];
|
||||
final List<ViewPB> publishedPages = [];
|
||||
|
||||
for (final view in views) {
|
||||
final publishInfo = await ViewBackendService.getPublishInfo(view);
|
||||
if (publishInfo.isSuccess) {
|
||||
publishedPages.add(view);
|
||||
}
|
||||
}
|
||||
|
||||
return (publishedPages.isNotEmpty, publishedPages);
|
||||
}
|
||||
}
|
||||
+15
@@ -15,6 +15,14 @@ class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
add(const ViewTitleBarEvent.reload());
|
||||
|
||||
viewListener = ViewListener(
|
||||
viewId: view.id,
|
||||
)..start(
|
||||
onViewUpdated: (p0) {
|
||||
add(const ViewTitleBarEvent.reload());
|
||||
},
|
||||
);
|
||||
},
|
||||
reload: () async {
|
||||
final List<ViewPB> ancestors =
|
||||
@@ -30,6 +38,13 @@ class ViewTitleBarBloc extends Bloc<ViewTitleBarEvent, ViewTitleBarState> {
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
late final ViewListener viewListener;
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
viewListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+2
-3
@@ -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,
|
||||
|
||||
+2
-2
@@ -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);
|
||||
|
||||
+8
-6
@@ -11,6 +11,8 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
typedef MovePageMenuOnSelected = void Function(ViewPB space, ViewPB view);
|
||||
|
||||
class MovePageMenu extends StatefulWidget {
|
||||
const MovePageMenu({
|
||||
super.key,
|
||||
@@ -23,7 +25,7 @@ class MovePageMenu extends StatefulWidget {
|
||||
final ViewPB sourceView;
|
||||
final UserProfilePB userProfile;
|
||||
final String workspaceId;
|
||||
final void Function(ViewPB view) onSelected;
|
||||
final MovePageMenuOnSelected onSelected;
|
||||
|
||||
@override
|
||||
State<MovePageMenu> createState() => _MovePageMenuState();
|
||||
@@ -88,7 +90,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
|
||||
);
|
||||
}
|
||||
return Expanded(
|
||||
child: _buildGroupedViews(state.queryResults!),
|
||||
child: _buildGroupedViews(space, state.queryResults!),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -99,7 +101,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGroupedViews(List<ViewPB> views) {
|
||||
Widget _buildGroupedViews(ViewPB space, List<ViewPB> views) {
|
||||
final groupedViews = views
|
||||
.where(
|
||||
(view) =>
|
||||
@@ -108,7 +110,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
|
||||
.toList();
|
||||
return _MovePageGroupedViews(
|
||||
views: groupedViews,
|
||||
onSelected: widget.onSelected,
|
||||
onSelected: (view) => widget.onSelected(space, view),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -124,7 +126,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
|
||||
child: CurrentSpace(
|
||||
onTapBlankArea: () {
|
||||
// move the page to current space
|
||||
widget.onSelected(space);
|
||||
widget.onSelected(space, space);
|
||||
},
|
||||
space: space,
|
||||
),
|
||||
@@ -145,7 +147,7 @@ class _MovePageMenuState extends State<MovePageMenu> {
|
||||
disableSelectedStatus: true,
|
||||
// hide the ... and + buttons
|
||||
rightIconsBuilder: (context, view) => [],
|
||||
onSelected: (_, view) => widget.onSelected(view),
|
||||
onSelected: (_, view) => widget.onSelected(space, view),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+17
-4
@@ -1,7 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/blank/blank.dart';
|
||||
@@ -32,11 +30,12 @@ import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/side
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart'
|
||||
show UserProfilePB;
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
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';
|
||||
|
||||
Loading? _duplicateSpaceLoading;
|
||||
@@ -360,10 +359,24 @@ class _SidebarState extends State<_Sidebar> {
|
||||
Widget _renderFolderOrSpace(EdgeInsets menuHorizontalInset) {
|
||||
final spaceState = context.read<SpaceBloc>().state;
|
||||
final workspaceState = context.read<UserWorkspaceBloc>().state;
|
||||
|
||||
if (!spaceState.isInitialized) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// there's no space or the workspace is not collaborative,
|
||||
// show the folder section (Workspace, Private, Personal)
|
||||
// otherwise, show the space
|
||||
return spaceState.spaces.isEmpty || !workspaceState.isCollabWorkspaceOn
|
||||
final sidebarSectionBloc = context.watch<SidebarSectionsBloc>();
|
||||
final containsSpace = sidebarSectionBloc.state.containsSpace;
|
||||
|
||||
if (containsSpace && spaceState.spaces.isEmpty) {
|
||||
context.read<SpaceBloc>().add(const SpaceEvent.didReceiveSpaceUpdate());
|
||||
}
|
||||
|
||||
return !containsSpace ||
|
||||
spaceState.spaces.isEmpty ||
|
||||
!workspaceState.isCollabWorkspaceOn
|
||||
? Expanded(
|
||||
child: Padding(
|
||||
padding: menuHorizontalInset - const EdgeInsets.only(right: 6),
|
||||
|
||||
+174
-44
@@ -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';
|
||||
@@ -19,6 +20,7 @@ import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class SpacePermissionSwitch extends StatefulWidget {
|
||||
@@ -222,58 +224,186 @@ class SpaceCancelOrConfirmButton extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class DeleteSpacePopup extends StatelessWidget {
|
||||
const DeleteSpacePopup({super.key});
|
||||
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) {
|
||||
final space = context.read<SpaceBloc>().state.currentSpace;
|
||||
final name = space != null ? space.name : '';
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 20.0,
|
||||
horizontal: 20.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
FlowyText(
|
||||
LocaleKeys.space_deleteConfirmation.tr() + name,
|
||||
fontSize: 14.0,
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: const FlowySvg(FlowySvgs.upgrade_close_s),
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
),
|
||||
],
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: confirmButtonColor ?? Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
const VSpace(8.0),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.space_deleteConfirmationDescription.tr(),
|
||||
fontSize: 12.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
maxLines: 3,
|
||||
lineHeight: 1.4,
|
||||
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,
|
||||
),
|
||||
const VSpace(20.0),
|
||||
SpaceCancelOrConfirmButton(
|
||||
onCancel: () => Navigator.of(context).pop(),
|
||||
onConfirm: () {
|
||||
context.read<SpaceBloc>().add(const SpaceEvent.delete(null));
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
confirmButtonName: LocaleKeys.space_delete.tr(),
|
||||
confirmButtonColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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<ConfirmPopup> createState() => _ConfirmPopupState();
|
||||
}
|
||||
|
||||
class _ConfirmPopupState extends State<ConfirmPopup> {
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return KeyboardListener(
|
||||
focusNode: focusNode,
|
||||
autofocus: true,
|
||||
onKeyEvent: (event) {
|
||||
if (event is KeyDownEvent &&
|
||||
event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 20.0,
|
||||
horizontal: 20.0,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildTitle(),
|
||||
const VSpace(6.0),
|
||||
_buildDescription(),
|
||||
const VSpace(20.0),
|
||||
_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
-11
@@ -227,18 +227,14 @@ class _SidebarSpaceHeaderState extends State<SidebarSpaceHeader> {
|
||||
|
||||
void _showDeleteSpaceDialog(BuildContext context) {
|
||||
final spaceBloc = context.read<SpaceBloc>();
|
||||
showDialog(
|
||||
final space = spaceBloc.state.currentSpace;
|
||||
final name = space != null ? space.name : '';
|
||||
showConfirmDeletionDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: BlocProvider.value(
|
||||
value: spaceBloc,
|
||||
child: const SizedBox(width: 440, child: DeleteSpacePopup()),
|
||||
),
|
||||
);
|
||||
name: name,
|
||||
description: LocaleKeys.space_deleteConfirmationDescription.tr(),
|
||||
onConfirm: () {
|
||||
context.read<SpaceBloc>().add(const SpaceEvent.delete(null));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
+6
-4
@@ -105,14 +105,16 @@ class _WorkspaceMoreActionWrapper extends CustomActionCell {
|
||||
case WorkspaceMoreAction.divider:
|
||||
break;
|
||||
case WorkspaceMoreAction.delete:
|
||||
await NavigatorAlertDialog(
|
||||
title: LocaleKeys.workspace_deleteWorkspaceHintText.tr(),
|
||||
confirm: () {
|
||||
await showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: workspace.name,
|
||||
description: LocaleKeys.workspace_deleteWorkspaceHintText.tr(),
|
||||
onConfirm: () {
|
||||
workspaceBloc.add(
|
||||
UserWorkspaceEvent.deleteWorkspace(workspace.workspaceId),
|
||||
);
|
||||
},
|
||||
).show(context);
|
||||
);
|
||||
case WorkspaceMoreAction.rename:
|
||||
await NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.workspace_renameWorkspace.tr(),
|
||||
|
||||
+53
-18
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_picker.dart';
|
||||
@@ -5,6 +7,7 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/rename_view/rename_view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
@@ -17,9 +20,10 @@ import 'package:appflowy/workspace/presentation/home/menu/view/view_add_button.d
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_more_action_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/rename_view_popover.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -334,6 +338,7 @@ class _InnerViewItemState extends State<InnerViewItem> {
|
||||
onMove: widget.isPlaceholder
|
||||
? (from, to) => _moveViewCrossSection(
|
||||
context,
|
||||
null,
|
||||
widget.view,
|
||||
widget.parentView,
|
||||
widget.spaceType,
|
||||
@@ -690,7 +695,7 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
spaceType: widget.spaceType,
|
||||
onEditing: (value) =>
|
||||
context.read<ViewBloc>().add(ViewEvent.setIsEditing(value)),
|
||||
onAction: (action, data) {
|
||||
onAction: (action, data) async {
|
||||
switch (action) {
|
||||
case ViewMoreActionType.favorite:
|
||||
case ViewMoreActionType.unFavorite:
|
||||
@@ -699,18 +704,36 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
.add(FavoriteEvent.toggle(widget.view));
|
||||
break;
|
||||
case ViewMoreActionType.rename:
|
||||
NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.disclosureAction_rename.tr(),
|
||||
autoSelectAllText: true,
|
||||
value: widget.view.name,
|
||||
maxLength: 256,
|
||||
onConfirm: (newValue, _) {
|
||||
context.read<ViewBloc>().add(ViewEvent.rename(newValue));
|
||||
},
|
||||
).show(context);
|
||||
unawaited(
|
||||
NavigatorTextFieldDialog(
|
||||
title: LocaleKeys.disclosureAction_rename.tr(),
|
||||
autoSelectAllText: true,
|
||||
value: widget.view.name,
|
||||
maxLength: 256,
|
||||
onConfirm: (newValue, _) {
|
||||
context.read<ViewBloc>().add(ViewEvent.rename(newValue));
|
||||
},
|
||||
).show(context),
|
||||
);
|
||||
break;
|
||||
case ViewMoreActionType.delete:
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
// get if current page contains published child views
|
||||
final (containPublishedPage, _) =
|
||||
await ViewBackendService.containPublishedPage(
|
||||
widget.view,
|
||||
);
|
||||
if (containPublishedPage && context.mounted) {
|
||||
await showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: widget.view.name,
|
||||
description: LocaleKeys.publish_containsPublishedPage.tr(),
|
||||
onConfirm: () {
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
},
|
||||
);
|
||||
} else if (context.mounted) {
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
}
|
||||
break;
|
||||
case ViewMoreActionType.duplicate:
|
||||
context.read<ViewBloc>().add(const ViewEvent.duplicate());
|
||||
@@ -726,22 +749,22 @@ class _SingleInnerViewItemState extends State<SingleInnerViewItem> {
|
||||
return;
|
||||
}
|
||||
final result = data;
|
||||
ViewBackendService.updateViewIcon(
|
||||
await ViewBackendService.updateViewIcon(
|
||||
viewId: widget.view.id,
|
||||
viewIcon: result.emoji,
|
||||
iconType: result.type.toProto(),
|
||||
);
|
||||
break;
|
||||
case ViewMoreActionType.moveTo:
|
||||
final target = data;
|
||||
if (target is! ViewPB) {
|
||||
final value = data;
|
||||
if (value is! (ViewPB, ViewPB)) {
|
||||
return;
|
||||
}
|
||||
debugPrint(
|
||||
'Move view ${widget.view.id}, ${widget.view.name} to ${target.id}, ${target.name}',
|
||||
);
|
||||
final space = value.$1;
|
||||
final target = value.$2;
|
||||
_moveViewCrossSection(
|
||||
context,
|
||||
space,
|
||||
widget.view,
|
||||
widget.parentView,
|
||||
widget.spaceType,
|
||||
@@ -802,6 +825,7 @@ bool isReferencedDatabaseView(ViewPB view, ViewPB? parentView) {
|
||||
|
||||
void _moveViewCrossSection(
|
||||
BuildContext context,
|
||||
ViewPB? toSpace,
|
||||
ViewPB view,
|
||||
ViewPB? parentView,
|
||||
FolderSpaceType spaceType,
|
||||
@@ -822,6 +846,17 @@ void _moveViewCrossSection(
|
||||
final toSection = spaceType == FolderSpaceType.public
|
||||
? ViewSectionPB.Public
|
||||
: ViewSectionPB.Private;
|
||||
|
||||
final currentSpace = context.read<SpaceBloc>().state.currentSpace;
|
||||
if (currentSpace != null &&
|
||||
toSpace != null &&
|
||||
currentSpace.id != toSpace.id) {
|
||||
Log.info(
|
||||
'Move view(${from.name}) to another space(${toSpace.name}), unpublish the view',
|
||||
);
|
||||
context.read<ViewBloc>().add(const ViewEvent.unpublish(sync: false));
|
||||
}
|
||||
|
||||
context.read<ViewBloc>().add(
|
||||
ViewEvent.move(
|
||||
from,
|
||||
|
||||
+2
-2
@@ -192,8 +192,8 @@ class ViewMoreActionTypeWrapper extends CustomActionCell {
|
||||
sourceView: sourceView,
|
||||
userProfile: userProfile,
|
||||
workspaceId: workspaceId,
|
||||
onSelected: (view) {
|
||||
onTap(controller, view);
|
||||
onSelected: (space, view) {
|
||||
onTap(controller, (space, view));
|
||||
},
|
||||
);
|
||||
},
|
||||
|
||||
+93
-3
@@ -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];
|
||||
|
||||
+108
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+75
@@ -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();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+314
@@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+85
@@ -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";
|
||||
}
|
||||
}
|
||||
+111
@@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+107
@@ -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()),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
-279
@@ -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,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+15
-7
@@ -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);
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
+5
-4
@@ -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(),
|
||||
|
||||
+2
-3
@@ -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(
|
||||
|
||||
+32
-7
@@ -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';
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+204
-18
@@ -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();
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ 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';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
@@ -10,6 +11,7 @@ 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:toastification/toastification.dart';
|
||||
|
||||
export 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
|
||||
|
||||
@@ -183,6 +185,7 @@ class NavigatorOkCancelDialog extends StatelessWidget {
|
||||
this.title,
|
||||
this.message,
|
||||
this.maxWidth,
|
||||
this.titleUpperCase = true,
|
||||
});
|
||||
|
||||
final VoidCallback? onOkPressed;
|
||||
@@ -192,6 +195,7 @@ class NavigatorOkCancelDialog extends StatelessWidget {
|
||||
final String? title;
|
||||
final String? message;
|
||||
final double? maxWidth;
|
||||
final bool titleUpperCase;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -203,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,
|
||||
),
|
||||
@@ -283,3 +287,118 @@ class OkCancelButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void showToastNotification(
|
||||
BuildContext context, {
|
||||
required String message,
|
||||
String? description,
|
||||
ToastificationType type = ToastificationType.success,
|
||||
}) {
|
||||
toastification.show(
|
||||
context: context,
|
||||
type: type,
|
||||
style: ToastificationStyle.flat,
|
||||
title: FlowyText(message),
|
||||
description: description != null
|
||||
? FlowyText.regular(
|
||||
description,
|
||||
fontSize: 12,
|
||||
lineHeight: 1.2,
|
||||
maxLines: 3,
|
||||
)
|
||||
: null,
|
||||
alignment: Alignment.bottomCenter,
|
||||
autoCloseDuration: const Duration(milliseconds: 3000),
|
||||
showProgressBar: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
borderSide: BorderSide(
|
||||
color: Colors.grey.withOpacity(0.4),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> showConfirmDeletionDialog({
|
||||
required BuildContext context,
|
||||
required String name,
|
||||
required String description,
|
||||
required VoidCallback onConfirm,
|
||||
}) {
|
||||
return showDialog(
|
||||
context: context,
|
||||
builder: (_) {
|
||||
final title = LocaleKeys.space_deleteConfirmation.tr() + name;
|
||||
return Dialog(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
),
|
||||
child: SizedBox(
|
||||
width: 440,
|
||||
child: ConfirmPopup(
|
||||
title: title,
|
||||
description: description,
|
||||
onConfirm: onConfirm,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
+21
-3
@@ -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(
|
||||
|
||||
+26
-4
@@ -1,14 +1,15 @@
|
||||
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/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.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:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
enum ViewActionType {
|
||||
@@ -46,8 +47,8 @@ class ViewAction extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyButton(
|
||||
onTap: () {
|
||||
context.read<ViewBloc>().add(type.actionEvent);
|
||||
onTap: () async {
|
||||
await _onAction(context);
|
||||
mutex?.close();
|
||||
},
|
||||
text: FlowyText.regular(
|
||||
@@ -63,4 +64,25 @@ class ViewAction extends StatelessWidget {
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onAction(BuildContext context) async {
|
||||
if (type == ViewActionType.delete) {
|
||||
final (containPublishedPage, _) =
|
||||
await ViewBackendService.containPublishedPage(view);
|
||||
if (containPublishedPage && context.mounted) {
|
||||
await showConfirmDeletionDialog(
|
||||
context: context,
|
||||
name: view.name,
|
||||
description: LocaleKeys.publish_containsPublishedPage.tr(),
|
||||
onConfirm: () {
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
},
|
||||
);
|
||||
} else if (context.mounted) {
|
||||
context.read<ViewBloc>().add(const ViewEvent.delete());
|
||||
}
|
||||
} else {
|
||||
context.read<ViewBloc>().add(type.actionEvent);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,9 @@ class ViewTitleBar extends StatelessWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: SizedBox(
|
||||
height: 24,
|
||||
child: Row(children: _buildViewTitles(context, ancestors)),
|
||||
child: Row(
|
||||
children: _buildViewTitles(context, ancestors),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -76,6 +78,7 @@ class ViewTitleBar extends StatelessWidget {
|
||||
}
|
||||
|
||||
final child = FlowyTooltip(
|
||||
key: ValueKey(view.id),
|
||||
message: view.name,
|
||||
child: _ViewTitle(
|
||||
view: view,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:flowy_infra/size.dart';
|
||||
|
||||
class FlowyTextField extends StatefulWidget {
|
||||
final String? hintText;
|
||||
final String? text;
|
||||
@@ -37,6 +36,8 @@ class FlowyTextField extends StatefulWidget {
|
||||
final List<TextInputFormatter>? inputFormatters;
|
||||
final bool obscureText;
|
||||
final bool isDense;
|
||||
final bool readOnly;
|
||||
final Color? enableBorderColor;
|
||||
|
||||
const FlowyTextField({
|
||||
super.key,
|
||||
@@ -71,6 +72,8 @@ class FlowyTextField extends StatefulWidget {
|
||||
this.inputFormatters,
|
||||
this.obscureText = false,
|
||||
this.isDense = true,
|
||||
this.readOnly = false,
|
||||
this.enableBorderColor,
|
||||
});
|
||||
|
||||
@override
|
||||
@@ -144,6 +147,7 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextField(
|
||||
readOnly: widget.readOnly,
|
||||
controller: controller,
|
||||
focusNode: focusNode,
|
||||
onChanged: (text) {
|
||||
@@ -178,7 +182,8 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: Corners.s8Border,
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
color: widget.enableBorderColor ??
|
||||
Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
isDense: false,
|
||||
@@ -199,7 +204,10 @@ class FlowyTextFieldState extends State<FlowyTextField> {
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: Corners.s8Border,
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
color: widget.readOnly
|
||||
? widget.enableBorderColor ??
|
||||
Theme.of(context).colorScheme.outline
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
|
||||
@@ -13,6 +13,7 @@ class RoundedTextButton extends StatelessWidget {
|
||||
final Color? hoverColor;
|
||||
final Color? textColor;
|
||||
final double? fontSize;
|
||||
final FontWeight? fontWeight;
|
||||
final EdgeInsets padding;
|
||||
|
||||
const RoundedTextButton({
|
||||
@@ -27,6 +28,7 @@ class RoundedTextButton extends StatelessWidget {
|
||||
this.hoverColor,
|
||||
this.textColor,
|
||||
this.fontSize,
|
||||
this.fontWeight,
|
||||
this.padding = const EdgeInsets.symmetric(horizontal: 8, vertical: 6),
|
||||
});
|
||||
|
||||
@@ -42,6 +44,7 @@ class RoundedTextButton extends StatelessWidget {
|
||||
child: SizedBox.expand(
|
||||
child: FlowyTextButton(
|
||||
title ?? '',
|
||||
fontWeight: fontWeight,
|
||||
onPressed: onPressed,
|
||||
fontSize: fontSize,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
|
||||
@@ -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:
|
||||
@@ -394,6 +410,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
custom_sliding_segmented_control:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: custom_sliding_segmented_control
|
||||
sha256: "53c3e931c3ae1f696085d1ec70ac8e934da836595a9b7d9b88fdd0fcbf2a5574"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.8.3"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -410,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:
|
||||
@@ -578,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:
|
||||
@@ -949,6 +997,22 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
iconsax_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: iconsax_flutter
|
||||
sha256: "95b65699da8ea98f87c5d232f06b0debaaf1ec1332b697e4d90969ec9a93037d"
|
||||
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:
|
||||
@@ -1187,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:
|
||||
@@ -1203,7 +1267,7 @@ packages:
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
markdown:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: markdown
|
||||
sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051
|
||||
@@ -1410,6 +1474,30 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
pausable_timer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: pausable_timer
|
||||
sha256: "6ef1a95441ec3439de6fb63f39a011b67e693198e7dae14e20675c3c00e86074"
|
||||
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:
|
||||
@@ -1530,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:
|
||||
@@ -1570,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:
|
||||
@@ -2049,6 +2153,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.1"
|
||||
toastification:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: toastification
|
||||
sha256: "5e751acc2fb5b8d008138dac255d62290fde4e5a24824f29809ac098c3dfe395"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
||||
@@ -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.2
|
||||
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
|
||||
@@ -149,6 +153,8 @@ dependencies:
|
||||
bitsdojo_window: ^0.1.6
|
||||
|
||||
flutter_highlight: ^0.7.0
|
||||
custom_sliding_segmented_control: ^1.8.3
|
||||
toastification: ^2.0.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_lints: ^3.0.1
|
||||
@@ -185,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:
|
||||
|
||||
+371
-124
File diff suppressed because it is too large.
Load diff
@@ -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
|
||||
@@ -106,10 +107,26 @@ default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
|
||||
[patch.crates-io]
|
||||
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" }
|
||||
# Please use the following script to update collab.
|
||||
# Working directory: frontend
|
||||
#
|
||||
# To update the commit ID, run:
|
||||
# scripts/tool/update_collab_rev.sh new_rev_id
|
||||
#
|
||||
# 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 = "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 = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
|
||||
appflowy-plugin = { version = "0.1", git = "https://github.com/AppFlowy-IO/AppFlowy-LocalAI", rev = "346020270a0a3d3c82a60b545cd3f52144d56beb" }
|
||||
Loaded 100 of 483 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user