Compare commits

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

* chore: remove check logic

* feat: integrate fix space into clear cache

* fix: translation missing value

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

* chore: flutter analyzer

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

* chore: config download ui

* chore: update zip

* chore: config download ui

* chore: unzip file

* chore: unzip file

* chore: rename

* chore: disable local ai

* chore: fmt

* chore: fix warning

* chore: update

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

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

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

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

* feat: support copy code block from ai chat page

* fix: add hover effect to color picker button

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

* chore: downgrade macos builder version to support x86_64

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

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

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

This commit addresses issue #5041

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

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

This commit addresses issue #5041

* chore: refactor code

---------

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

* chore: update appflowy_editor

* fix: integration test

* feat: support paste inAppJson format

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

* chore: fix clippy errors

* chore: update yrs and collab dependencies in appflowy tauri

* chore: update yrs and collab dependencies in appflowy web app
2024-07-10 10:10:21 +08:00
Lucas.Xu 655a2b7093 chore: bump version 0.6.3 (#5701) 2024-07-08 13:46:27 +08:00
23c67bcdba feat: support publish document (#5576)
* feat: support a event for getting encoded collab of document

* feat: support publish view and unpublish views

* feat: publish page to the web

* chore: refacotor share bloc

* feat: call the publish event

* feat: support publish view and unpublish views

* feat: integrate publish api

* feat: integrate unpublish api

* feat: fetch the publish info to show the publish status

* feat: support publish interfaces

* fix: lint error

* fix: modified web server

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: update codes

* fix: update codes

* fix: update codes

* fix: update codes

* fix: update codes

* chore: refactor publish bloc

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: some style

* fix: the name is too long to publish

* chore: change color

* fix: some style

* fix: some style

* feat: refacotor share menu UI

* fix: some style

* fix: lint

* fix: some style

* feat: refacotor export-as

* fix: some style

* chore: refactor share menu colors

* fix: rust ci

* fix: some style

* fix: some style

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: bugs

* fix: rerelease

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: deploy

* fix: og image

* fix: support copy button

* fix: support copy button

* fix: support copy button

* chore: add a params

* feat: use default publish name

* chore: update copy

* feat: show a confirm deletion dialog if the deleted page contains published page

* feat: add copy toast in publish tab

* fix: to 404

fix: to 404

fix: to 404

fix: the error to 404

* feat: unpublish the page auto when moving it to another space

* feat: improve confirm deletion dialog

* feat: show unpublish error

* chore: use beta.appflowy.com

* feat: disable publish in non-apppflowy-cloud user mode

* fix: modified bullted icon style

* fix: the dark mode color

* fix: save the dark mode in local storage

* fix: text color

* chore: make bash script more portable (#5679)

* fix: title longer

* chore: move the files and modified the en

* chore: update deploy.sh

* chore: modified Dockerfile

* chore: modified server.cjs to server.js

* chore: modifed server.js to server.ts

* chore: replace publish url

* chore: remove todo list hover

* chore: show confirm dialog before deleting page

* fix: unpublish the pages before deleting

* fix: table cell bg color

* fix: callout icon

* fix: list number

* fix: emoji

* fix: number icon

* fix: callout icon position

* fix: add margin bottom

* fix: code block

* fix: support scroll for breadcrumbs

* fix: the breadcrumb doesn't update after moving page

* fix: 0705 issues

* fix: update publish status afer deleting page

* chore: add hover effect for visit site button

* fix: remove puiblish url text field enable border color

* chore: update delete page copy

* chore: enable debug category

* fix: only render sidebar if the spaces are ready

* fix: the breadcrumb doesn't update after moving page

* fix: auto code

* fix: add emoji

* fix: add emoji

* fix: favicon

* fix: cypress test

* fix: remove deploy ci

* fix: default url

* chore: revert launch.json

* fix: docker ci

* fix: change favicon

* fix: flutter integration test

* feat: add hover effect to share menu

* chore: add a checkmark if the page has been published

* chore: revert space deletion

---------

Co-authored-by: Lucas.Xu <lucas.xu@appflowy.io>
Co-authored-by: Zack <speed2exe@live.com.sg>
2024-07-08 13:45:57 +08:00
Nathan.fooo 521fffd97c refactor: extract chat plugin to new repo (#5699)
* chore: update ui

* chore: update

* chore: update

* chore: separate to new crate

* chore: update commit id

* chore: fix compile

* chore: bump version
2024-07-08 13:19:13 +08:00
Lucas.Xu 2ecc2a67a9 fix: take the max value of the keyboard height and the view insets bottom to make the toolbar visible (#5700)
* fix: android toolbar will be blocked if the scroll bar is hidden

* fix: last opened view is synced in same workspace

* fix: fallback to space if the section sidebar contains space

* chore: revert last opened view code

* fix: integration tests
2024-07-08 13:15:42 +08:00
Lucas.Xu 2d11215bb2 chore: use latest macos runner to build the release package (#5686) 2024-07-08 09:43:23 +08:00
483 changed files with 13308 additions and 8933 deletions

No files matched your search

-72
View File
@@ -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
+2 -2
View File
@@ -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:
+12
View File
@@ -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.
+1 -1
View File
@@ -138,4 +138,4 @@
"cwd": "${workspaceRoot}/appflowy_tauri/"
},
]
}
}
+1 -1
View File
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
APPFLOWY_VERSION = "0.6.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);
});
});
}
@@ -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,
);
});
@@ -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
+8 -2
View File
@@ -65,6 +65,8 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- printing (1.0.0):
- Flutter
- ReachabilitySwift (5.0.0)
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
@@ -101,6 +103,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- printing (from `.symlinks/plugins/printing/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@@ -149,6 +152,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
printing:
:path: ".symlinks/plugins/printing/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -170,7 +175,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
@@ -179,6 +184,7 @@ SPEC CHECKSUMS:
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
@@ -191,4 +197,4 @@ SPEC CHECKSUMS:
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
COCOAPODS: 1.11.3
COCOAPODS: 1.15.2
+7
View File
@@ -29,4 +29,11 @@ abstract class Env {
defaultValue: '',
)
static const String afCloudUrl = _Env.afCloudUrl;
@EnviedField(
obfuscate: false,
varName: 'INTERNAL_BUILD',
defaultValue: '',
)
static const String internalBuild = _Env.internalBuild;
}
@@ -38,8 +38,7 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
builder: (context, state) {
final ids = <String>{};
List<ViewPB> recentViews =
state.views.reversed.map((e) => e.item).toList();
List<ViewPB> recentViews = state.views.map((e) => e.item).toList();
recentViews.retainWhere((element) => ids.add(element.id));
// only keep the first 20 items.
@@ -49,7 +49,7 @@ class _MobileRecentSpaceState extends State<MobileRecentSpace>
List<SectionViewPB> _filterRecentViews(List<SectionViewPB> recentViews) {
final ids = <String>{};
final filteredRecentViews = recentViews.reversed.toList();
final filteredRecentViews = recentViews.toList();
filteredRecentViews.retainWhere((e) => ids.add(e.item.id));
return filteredRecentViews;
}
@@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget {
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'),
onTap: () => afLaunchUrlString('https://appflowy.io/privacy'),
),
MobileSettingItem(
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
trailing: const Icon(
Icons.chevron_right,
),
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
onTap: () => afLaunchUrlString('https://appflowy.io/terms'),
),
if (kDebugMode)
MobileSettingItem(
@@ -7,6 +7,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
@@ -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';
@@ -0,0 +1,373 @@
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart';
enum AIMarkdownType {
appflowyEditor,
markdownWidget,
}
// Wrap the appflowy_editor or markdown_widget as a chat text message widget
class AIMarkdownText extends StatelessWidget {
const AIMarkdownText({
super.key,
required this.markdown,
this.type = AIMarkdownType.appflowyEditor,
});
final String markdown;
final AIMarkdownType type;
@override
Widget build(BuildContext context) {
switch (type) {
case AIMarkdownType.appflowyEditor:
return _AppFlowyEditorMarkdown(markdown: markdown);
case AIMarkdownType.markdownWidget:
return _ThirdPartyMarkdown(markdown: markdown);
}
}
}
class _AppFlowyEditorMarkdown extends StatefulWidget {
const _AppFlowyEditorMarkdown({
required this.markdown,
});
// the text should be the markdown format
final String markdown;
@override
State<_AppFlowyEditorMarkdown> createState() =>
_AppFlowyEditorMarkdownState();
}
class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
late EditorState editorState;
late final styleCustomizer = EditorStyleCustomizer(
context: context,
padding: EdgeInsets.zero,
);
late final editorStyle = styleCustomizer.style().copyWith(
// hide the cursor
cursorColor: Colors.transparent,
cursorWidth: 0,
);
late EditorScrollController scrollController;
@override
void initState() {
super.initState();
editorState = _parseMarkdown(widget.markdown);
scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
}
@override
void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.markdown != widget.markdown) {
editorState.dispose();
editorState = _parseMarkdown(widget.markdown);
scrollController.dispose();
scrollController = EditorScrollController(
editorState: editorState,
shrinkWrap: true,
);
}
}
@override
void dispose() {
scrollController.dispose();
editorState.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final blockBuilders = getEditorBuilderMap(
context: context,
editorState: editorState,
styleCustomizer: styleCustomizer,
// the editor is not editable in the chat
editable: false,
);
return IntrinsicHeight(
child: AppFlowyEditor(
shrinkWrap: true,
// the editor is not editable in the chat
editable: false,
editorStyle: editorStyle,
editorScrollController: scrollController,
blockComponentBuilders: blockBuilders,
commandShortcutEvents: [customCopyCommand],
editorState: editorState,
),
);
}
EditorState _parseMarkdown(String markdown) {
final document = customMarkdownToDocument(markdown);
final editorState = EditorState(document: document);
return editorState;
}
}
class _ThirdPartyMarkdown extends StatelessWidget {
const _ThirdPartyMarkdown({
required this.markdown,
});
final String markdown;
@override
Widget build(BuildContext context) {
return MarkdownWidget(
data: markdown,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
_ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
_ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
_ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
builder: (code, language) {
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 800,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
child: SelectableHighlightView(
code,
language: language,
theme: getHighlightTheme(context),
padding: const EdgeInsets.all(14),
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
),
);
},
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig(
sideColor: AFThemeExtension.of(context).lightGreyHover,
textColor: AFThemeExtension.of(context).textColor,
),
],
);
}
Map<String, TextStyle> getHighlightTheme(BuildContext context) {
return {
'root': TextStyle(
color: const Color(0xffabb2bf),
backgroundColor:
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
),
'comment': const TextStyle(
color: Color(0xff5c6370),
fontStyle: FontStyle.italic,
),
'quote': const TextStyle(
color: Color(0xff5c6370),
fontStyle: FontStyle.italic,
),
'doctag': const TextStyle(color: Color(0xffc678dd)),
'keyword': const TextStyle(color: Color(0xffc678dd)),
'formula': const TextStyle(color: Color(0xffc678dd)),
'section': const TextStyle(color: Color(0xffe06c75)),
'name': const TextStyle(color: Color(0xffe06c75)),
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
'deletion': const TextStyle(color: Color(0xffe06c75)),
'subst': const TextStyle(color: Color(0xffe06c75)),
'literal': const TextStyle(color: Color(0xff56b6c2)),
'string': const TextStyle(color: Color(0xff98c379)),
'regexp': const TextStyle(color: Color(0xff98c379)),
'addition': const TextStyle(color: Color(0xff98c379)),
'attribute': const TextStyle(color: Color(0xff98c379)),
'meta-string': const TextStyle(color: Color(0xff98c379)),
'built_in': const TextStyle(color: Color(0xffe6c07b)),
'attr': const TextStyle(color: Color(0xffd19a66)),
'variable': const TextStyle(color: Color(0xffd19a66)),
'template-variable': const TextStyle(color: Color(0xffd19a66)),
'type': const TextStyle(color: Color(0xffd19a66)),
'selector-class': const TextStyle(color: Color(0xffd19a66)),
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
'number': const TextStyle(color: Color(0xffd19a66)),
'symbol': const TextStyle(color: Color(0xff61aeee)),
'bullet': const TextStyle(color: Color(0xff61aeee)),
'link': const TextStyle(color: Color(0xff61aeee)),
'meta': const TextStyle(color: Color(0xff61aeee)),
'selector-id': const TextStyle(color: Color(0xff61aeee)),
'title': const TextStyle(color: Color(0xff61aeee)),
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
'strong': const TextStyle(fontWeight: FontWeight.bold),
};
}
}
class _ChatH1Config extends HeadingConfig {
const _ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class _ChatH2Config extends HeadingConfig {
const _ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class _ChatH3Config extends HeadingConfig {
const _ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
@@ -2,19 +2,15 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_chat_types/flutter_chat_types.dart';
import 'package:markdown_widget/markdown_widget.dart';
import 'selectable_highlight.dart';
class ChatAITextMessageWidget extends StatelessWidget {
const ChatAITextMessageWidget({
@@ -59,248 +55,12 @@ class ChatAITextMessageWidget extends StatelessWidget {
if (state.text.isEmpty) {
return const ChatAILoading();
} else {
return _textWidgetBuilder(user, context, state.text);
return AIMarkdownText(markdown: state.text);
}
},
),
);
}
Widget _textWidgetBuilder(
User user,
BuildContext context,
String text,
) {
return MarkdownWidget(
data: text,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
config: configFromContext(context),
);
}
MarkdownConfig configFromContext(BuildContext context) {
return MarkdownConfig(
configs: [
HrConfig(color: AFThemeExtension.of(context).textColor),
ChatH1Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 24,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH2Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 20,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
ChatH3Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 18,
fontWeight: FontWeight.bold,
height: 1.5,
),
dividerColor: AFThemeExtension.of(context).lightGreyHover,
),
H4Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H5Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
H6Config(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 12,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
PreConfig(
builder: (code, language) {
return ConstrainedBox(
constraints: const BoxConstraints(
minWidth: 800,
),
child: ClipRRect(
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
child: SelectableHighlightView(
code,
language: language,
theme: getHightlineTheme(context),
padding: const EdgeInsets.all(14),
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 14,
fontWeight: FontWeight.bold,
height: 1.5,
),
),
),
);
},
),
PConfig(
textStyle: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
CodeConfig(
style: TextStyle(
color: AFThemeExtension.of(context).textColor,
fontSize: 16,
fontWeight: FontWeight.w500,
height: 1.5,
),
),
BlockquoteConfig(
sideColor: AFThemeExtension.of(context).lightGreyHover,
textColor: AFThemeExtension.of(context).textColor,
),
],
);
}
}
Map<String, TextStyle> getHightlineTheme(BuildContext context) {
return {
'root': TextStyle(
color: const Color(0xffabb2bf),
backgroundColor:
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
),
'comment':
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
'quote':
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
'doctag': const TextStyle(color: Color(0xffc678dd)),
'keyword': const TextStyle(color: Color(0xffc678dd)),
'formula': const TextStyle(color: Color(0xffc678dd)),
'section': const TextStyle(color: Color(0xffe06c75)),
'name': const TextStyle(color: Color(0xffe06c75)),
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
'deletion': const TextStyle(color: Color(0xffe06c75)),
'subst': const TextStyle(color: Color(0xffe06c75)),
'literal': const TextStyle(color: Color(0xff56b6c2)),
'string': const TextStyle(color: Color(0xff98c379)),
'regexp': const TextStyle(color: Color(0xff98c379)),
'addition': const TextStyle(color: Color(0xff98c379)),
'attribute': const TextStyle(color: Color(0xff98c379)),
'meta-string': const TextStyle(color: Color(0xff98c379)),
'built_in': const TextStyle(color: Color(0xffe6c07b)),
'attr': const TextStyle(color: Color(0xffd19a66)),
'variable': const TextStyle(color: Color(0xffd19a66)),
'template-variable': const TextStyle(color: Color(0xffd19a66)),
'type': const TextStyle(color: Color(0xffd19a66)),
'selector-class': const TextStyle(color: Color(0xffd19a66)),
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
'number': const TextStyle(color: Color(0xffd19a66)),
'symbol': const TextStyle(color: Color(0xff61aeee)),
'bullet': const TextStyle(color: Color(0xff61aeee)),
'link': const TextStyle(color: Color(0xff61aeee)),
'meta': const TextStyle(color: Color(0xff61aeee)),
'selector-id': const TextStyle(color: Color(0xff61aeee)),
'title': const TextStyle(color: Color(0xff61aeee)),
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
'strong': const TextStyle(fontWeight: FontWeight.bold),
};
}
class ChatH1Config extends HeadingConfig {
const ChatH1Config({
this.style = const TextStyle(
fontSize: 32,
height: 40 / 32,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h1.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
///config class for h2
class ChatH2Config extends HeadingConfig {
const ChatH2Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h2.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class ChatH3Config extends HeadingConfig {
const ChatH3Config({
this.style = const TextStyle(
fontSize: 24,
height: 30 / 24,
fontWeight: FontWeight.bold,
),
required this.dividerColor,
});
@override
final TextStyle style;
final Color dividerColor;
@override
String get tag => MarkdownTag.h3.name;
@override
HeadingDivider? get divider => HeadingDivider(
space: 10,
color: dividerColor,
height: 10,
);
}
class StreamingError extends StatelessWidget {
@@ -52,7 +52,6 @@ class TextMessageText extends StatelessWidget {
text,
fontSize: 16,
fontWeight: FontWeight.w500,
lineHeight: 1.5,
maxLines: null,
selectable: true,
color: AFThemeExtension.of(context).textColor,
@@ -52,7 +52,7 @@ class DatabaseTabBarBloc
_createLinkedView(layout.layoutType, name ?? layout.layoutName);
},
deleteView: (String viewId) async {
final result = await ViewBackendService.delete(viewId: viewId);
final result = await ViewBackendService.deleteView(viewId: viewId);
result.fold(
(l) {},
(r) => Log.error(r),
@@ -1,6 +1,6 @@
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -1,7 +1,7 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flutter/material.dart';
import 'package:percent_indicator/percent_indicator.dart';
import '../../application/cell/bloc/checklist_cell_bloc.dart';
class ChecklistProgressBar extends StatefulWidget {
@@ -68,7 +68,7 @@ class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
),
),
SizedBox(
width: PlatformExtension.isDesktop ? 36 : 45,
width: 45,
child: Align(
alignment: AlignmentDirectional.centerEnd,
child: Text(
@@ -18,6 +18,7 @@ import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/util/debounce.dart';
import 'package:appflowy/util/throttle.dart';
import 'package:appflowy/workspace/application/view/view_listener.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
@@ -37,6 +38,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
part 'document_bloc.freezed.dart';
bool enableDocumentInternalLog = false;
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
DocumentBloc({
required this.documentId,
@@ -212,6 +215,10 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
}
Future<EditorState?> _initAppFlowyEditorState(DocumentDataPB data) async {
if (enableDocumentInternalLog) {
Log.info('document data: ${data.toProto3Json()}');
}
final document = data.toDocument();
if (document == null) {
assert(false, 'document is null');
@@ -180,6 +180,8 @@ extension NodeToBlock on Node {
String? parentId,
String? childrenId,
Attributes? attributes,
String? externalId,
String? externalType,
}) {
assert(id.isNotEmpty);
final block = BlockPB.create()
@@ -192,6 +194,12 @@ extension NodeToBlock on Node {
if (parentId != null && parentId.isNotEmpty) {
block.parentId = parentId;
}
if (externalId != null && externalId.isNotEmpty) {
block.externalId = externalId;
}
if (externalType != null && externalType.isNotEmpty) {
block.externalType = externalType;
}
return block;
}
@@ -1,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: '',
);
}
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy_backend/log.dart';
@@ -21,6 +22,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
import 'package:collection/collection.dart';
import 'package:nanoid/nanoid.dart';
const _kExternalTextType = 'text';
/// Uses to adjust the data structure between the editor and the backend.
///
/// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
@@ -34,11 +37,9 @@ class TransactionAdapter {
final DocumentService documentService;
final String documentId;
final bool _enableDebug = false;
Future<void> apply(Transaction transaction, EditorState editorState) async {
final stopwatch = Stopwatch()..start();
if (_enableDebug) {
if (enableDocumentInternalLog) {
Log.debug('transaction => ${transaction.toJson()}');
}
final actions = transaction.operations
@@ -60,8 +61,10 @@ class TransactionAdapter {
textId: payload.textId,
delta: payload.delta,
);
if (_enableDebug) {
Log.debug('create external text: ${payload.delta}');
if (enableDocumentInternalLog) {
Log.debug(
'[editor_transaction_adapter] create external text: ${payload.delta}',
);
}
} else if (type == TextDeltaType.update) {
await documentService.updateExternalText(
@@ -69,8 +72,10 @@ class TransactionAdapter {
textId: payload.textId,
delta: payload.delta,
);
if (_enableDebug) {
Log.debug('update external text: ${payload.delta}');
if (enableDocumentInternalLog) {
Log.debug(
'[editor_transaction_adapter] update external text: ${payload.delta}',
);
}
}
}
@@ -82,9 +87,9 @@ class TransactionAdapter {
);
final elapsed = stopwatch.elapsedMilliseconds;
stopwatch.stop();
if (_enableDebug) {
if (enableDocumentInternalLog) {
Log.debug(
'apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
'[editor_transaction_adapter] apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
);
}
}
@@ -136,8 +141,9 @@ extension on InsertOperation {
// create the external text if the node contains the delta in its data.
final delta = node.delta;
TextDeltaPayloadPB? textDeltaPayloadPB;
String? textId;
if (delta != null) {
final textId = nanoid(6);
textId = nanoid(6);
textDeltaPayloadPB = TextDeltaPayloadPB(
documentId: documentId,
@@ -148,13 +154,17 @@ extension on InsertOperation {
// sync the text id to the node
node.externalValues = ExternalValues(
externalId: textId,
externalType: 'text',
externalType: _kExternalTextType,
);
}
// remove the delta from the data when the incremental update is stable.
final payload = BlockActionPayloadPB()
..block = node.toBlock(childrenId: nanoid(6))
..block = node.toBlock(
childrenId: nanoid(6),
externalId: textId,
externalType: textId != null ? _kExternalTextType : null,
)
..parentId = parentId
..prevId = prevId;
@@ -248,7 +258,7 @@ extension on UpdateOperation {
node.externalValues = ExternalValues(
externalId: textId,
externalType: 'text',
externalType: _kExternalTextType,
);
actions.add(
@@ -1,16 +1,17 @@
import 'package:flutter/material.dart';
import 'dart:convert';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
CodeBlockCopyBuilder codeBlockCopyBuilder =
(_, node) => _CopyButton(node: node);
@@ -28,16 +29,26 @@ class _CopyButton extends StatelessWidget {
message: LocaleKeys.document_codeBlock_copyTooltip.tr(),
child: FlowyIconButton(
onPressed: () async {
final delta = node.delta;
if (delta == null) {
return;
}
final document = Document.blank()
..insert([0], [node.copyWith()])
..toJson();
await getIt<ClipboardService>().setData(
ClipboardServiceData(
plainText: node.delta?.toPlainText(),
plainText: delta.toPlainText(),
inAppJson: jsonEncode(document.toJson()),
),
);
if (context.mounted) {
showSnackBarMessage(
showToastNotification(
context,
LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
);
}
},
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
@@ -10,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
@@ -54,6 +53,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
// try to paste the content in order, if any of them is failed, then try the next one
if (inAppJson != null && inAppJson.isNotEmpty) {
debugPrint('paste in app json: $inAppJson');
await editorState.deleteSelectionIfNeeded();
if (await editorState.pasteInAppJson(inAppJson)) {
return;
@@ -75,7 +75,9 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
child: BlocConsumer<DocumentImmersiveCoverBloc,
DocumentImmersiveCoverState>(
listener: (context, state) {
textEditingController.text = state.name;
if (textEditingController.text.isEmpty) {
textEditingController.text = state.name;
}
},
builder: (_, state) {
final iconAndTitle = _buildIconAndTitle(context, state);
@@ -159,8 +161,16 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
state.cover.isNone || state.cover.isPresets ? null : Colors.white,
overflow: TextOverflow.ellipsis,
),
onChanged: _rename,
onSubmitted: _rename,
onChanged: (name) => Debounce.debounce(
'rename',
const Duration(milliseconds: 300),
() => _rename(name),
),
onSubmitted: (name) => Debounce.debounce(
'rename',
const Duration(milliseconds: 300),
() => _rename(name),
),
);
}
@@ -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,
@@ -0,0 +1,50 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
import 'package:markdown/markdown.dart' as md;
class MarkdownCodeBlockParser extends CustomMarkdownParser {
const MarkdownCodeBlockParser();
@override
List<Node> transform(
md.Node element,
List<CustomMarkdownParser> parsers, {
MarkdownListType listType = MarkdownListType.unknown,
int? startNumber,
}) {
if (element is! md.Element) {
return [];
}
if (element.tag != 'pre') {
return [];
}
final ec = element.children;
if (ec == null || ec.isEmpty) {
return [];
}
final code = ec.first;
if (code is! md.Element || code.tag != 'code') {
return [];
}
String? language;
if (code.attributes.containsKey('class')) {
final classes = code.attributes['class']!.split(' ');
final languageClass = classes.firstWhere(
(c) => c.startsWith('language-'),
orElse: () => '',
);
language = languageClass.substring('language-'.length);
}
return [
codeBlockNode(
language: language,
delta: Delta()..insert(code.textContent.trimRight()),
),
];
}
}
@@ -1,3 +1,4 @@
export 'callout_node_parser.dart';
export 'markdown_code_parser.dart';
export 'math_equation_node_parser.dart';
export 'toggle_list_node_parser.dart';
@@ -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,
);
}
}
@@ -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;
}
}
@@ -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,
),
),
),
);
}
}
@@ -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;
}
}
@@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/me
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
import 'package:appflowy/plugins/inline_actions/service_handler.dart';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -64,11 +65,9 @@ class InlinePageReferenceService extends InlineActionsDelegate {
_recentViewsInitialized = true;
final views = (await _recentService.recentViews())
.reversed
.map((e) => e.item)
.toSet()
.toList();
final sectionViews = await _recentService.recentViews();
final views =
sectionViews.unique((e) => e.item.id).map((e) => e.item).toList();
// Filter by viewLayout
views.retainWhere(
@@ -35,6 +35,9 @@ enum FeatureFlag {
// used for controlling whether to show plan+billing options in settings
planBilling,
// used for space design
spaceDesign,
// used for ignore the conflicted feature flag
unknown;
@@ -88,6 +91,8 @@ enum FeatureFlag {
bool get isOn {
if ([
// release this feature in version 0.6.1
FeatureFlag.spaceDesign,
// release this feature in version 0.5.9
FeatureFlag.search,
// release this feature in version 0.5.6
@@ -105,15 +110,16 @@ enum FeatureFlag {
}
switch (this) {
case FeatureFlag.search:
case FeatureFlag.syncDocument:
case FeatureFlag.syncDatabase:
case FeatureFlag.spaceDesign:
return true;
case FeatureFlag.collaborativeWorkspace:
case FeatureFlag.membersSettings:
case FeatureFlag.planBilling:
case FeatureFlag.unknown:
return false;
case FeatureFlag.search:
case FeatureFlag.syncDocument:
case FeatureFlag.syncDatabase:
return true;
}
}
@@ -131,6 +137,8 @@ enum FeatureFlag {
return 'if it\'s on, the command palette and search button will be available';
case FeatureFlag.planBilling:
return 'if it\'s on, plan and billing pages will be available in Settings';
case FeatureFlag.spaceDesign:
return 'if it\'s on, the space design feature will be available';
case FeatureFlag.unknown:
return '';
}
@@ -0,0 +1,11 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
Document customMarkdownToDocument(String markdown) {
return markdownToDocument(
markdown,
markdownParsers: [
const MarkdownCodeBlockParser(),
],
);
}
@@ -6,8 +6,8 @@ import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/startup/startup.dart';
class WindowSizeManager {
static const double minWindowHeight = 600.0;
static const double minWindowWidth = 800.0;
static const double minWindowHeight = 640.0;
static const double minWindowWidth = 960.0;
// Preventing failed assertion due to Texture Descriptor Validation
static const double maxWindowHeight = 8192.0;
static const double maxWindowWidth = 8192.0;
@@ -54,16 +54,8 @@ class DesktopSignInScreen extends StatelessWidget {
const SignInAnonymousButtonV2(),
const VSpace(10),
SwitchSignInSignUpButton(
onTap: () {
final type = state.loginType == LoginType.signIn
? LoginType.signUp
: LoginType.signIn;
context
.read<SignInBloc>()
.add(SignInEvent.switchLoginType(type));
},
),
// sign in agreement
const SignInAgreement(),
// loading status
const VSpace(indicatorMinHeight),
@@ -39,16 +39,7 @@ class MobileSignInScreen extends StatelessWidget {
const VSpace(spacing),
const SignInAnonymousButtonV2(),
const VSpace(spacing),
SwitchSignInSignUpButton(
onTap: () {
final type = state.loginType == LoginType.signIn
? LoginType.signUp
: LoginType.signIn;
context.read<SignInBloc>().add(
SignInEvent.switchLoginType(type),
);
},
),
const SignInAgreement(),
const VSpace(spacing),
_buildSettingsButton(context),
if (!isAuthEnabled) const Spacer(flex: 2),
@@ -1,14 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/application/sign_in_bloc.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:string_validator/string_validator.dart';
import 'package:toastification/toastification.dart';
class SignInWithMagicLinkButtons extends StatefulWidget {
const SignInWithMagicLinkButtons({super.key});
@@ -53,18 +53,19 @@ class _SignInWithMagicLinkButtonsState
void _sendMagicLink(BuildContext context, String email) {
if (!isEmail(email)) {
return showSnackBarMessage(
return showToastNotification(
context,
LocaleKeys.signIn_invalidEmail.tr(),
duration: const Duration(seconds: 8),
message: LocaleKeys.signIn_invalidEmail.tr(),
type: ToastificationType.error,
);
}
context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email));
showSnackBarMessage(
context,
LocaleKeys.signIn_magicLinkSent.tr(),
duration: const Duration(seconds: 1000),
showConfirmDialog(
context: context,
title: LocaleKeys.signIn_magicLinkSent.tr(),
description: LocaleKeys.signIn_magicLinkSentDescription.tr(),
);
}
}
@@ -0,0 +1,44 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
class SignInAgreement extends StatelessWidget {
const SignInAgreement({
super.key,
});
@override
Widget build(BuildContext context) {
return RichText(
textAlign: TextAlign.center,
text: TextSpan(
children: [
TextSpan(
text: '${LocaleKeys.web_signInAgreement.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
TextSpan(
text: '${LocaleKeys.web_termOfUse.tr()} ',
style: const TextStyle(color: Colors.blue, fontSize: 12),
mouseCursor: SystemMouseCursors.click,
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString('https://appflowy.io/terms'),
),
TextSpan(
text: '${LocaleKeys.web_and.tr()} ',
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
TextSpan(
text: LocaleKeys.web_privacyPolicy.tr(),
style: const TextStyle(color: Colors.blue, fontSize: 12),
mouseCursor: SystemMouseCursors.click,
recognizer: TapGestureRecognizer()
..onTap = () => afLaunchUrlString('https://appflowy.io/privacy'),
),
],
),
);
}
}
@@ -1,5 +1,7 @@
export 'magic_link_sign_in_buttons.dart';
export 'sign_in_anonymous_button.dart';
export 'sign_in_or_logout_button.dart';
export 'switch_sign_in_sign_up_button.dart';
// export 'switch_sign_in_sign_up_button.dart';
export 'third_party_sign_in_buttons.dart';
export 'sign_in_agreement.dart';
@@ -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(
@@ -1,5 +1,6 @@
import 'dart:async';
import 'package:appflowy/shared/list_extension.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/recent_listener.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -7,6 +8,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:fixnum/fixnum.dart';
import 'package:flutter/foundation.dart';
/// This is a lazy-singleton to share recent views across the application.
@@ -37,7 +39,7 @@ class CachedRecentService {
_listener.start(recentViewsUpdated: _recentViewsUpdated);
_recentViews = await _readRecentViews().fold(
(s) => s.items,
(s) => s.items.unique((e) => e.item.id),
(_) => [],
);
_completer.complete();
@@ -68,7 +70,8 @@ class CachedRecentService {
Future<FlowyResult<RepeatedRecentViewPB, FlowyError>>
_readRecentViews() async {
final result = await FolderEventReadRecentViews().send();
final payload = ReadRecentViewsPB(start: Int64(), limit: Int64(100));
final result = await FolderEventReadRecentViews(payload).send();
return result.fold(
(recentViews) {
return FlowyResult.success(
@@ -101,7 +104,7 @@ class CachedRecentService {
final viewIds = result.toNullable();
if (viewIds != null) {
_recentViews = await _readRecentViews().fold(
(s) => s.items,
(s) => s.items.unique((e) => e.item.id),
(_) => [],
);
}
@@ -0,0 +1,140 @@
import 'dart:async';
import 'dart:ffi';
import 'dart:isolate';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:fixnum/fixnum.dart';
part 'download_model_bloc.freezed.dart';
class DownloadModelBloc extends Bloc<DownloadModelEvent, DownloadModelState> {
DownloadModelBloc(LLMModelPB model)
: super(DownloadModelState(model: model)) {
on<DownloadModelEvent>(_handleEvent);
}
Future<void> _handleEvent(
DownloadModelEvent event,
Emitter<DownloadModelState> emit,
) async {
await event.when(
started: () async {
final downloadStream = DownloadingStream();
downloadStream.listen(
onModelPercentage: (name, percent) {
if (!isClosed) {
add(
DownloadModelEvent.updatePercent(name, percent),
);
}
},
onPluginPercentage: (percent) {
if (!isClosed) {
add(DownloadModelEvent.updatePercent("AppFlowy Plugin", percent));
}
},
onFinish: () {
add(const DownloadModelEvent.downloadFinish());
},
onError: (err) {
// emit(state.copyWith(downloadError: err));
},
);
final payload =
DownloadLLMPB(progressStream: Int64(downloadStream.nativePort));
final result = await ChatEventDownloadLLMResource(payload).send();
result.fold((_) {
emit(
state.copyWith(
downloadStream: downloadStream,
loadingState: const LoadingState.finish(),
downloadError: null,
),
);
}, (err) {
emit(state.copyWith(loadingState: LoadingState.finish(error: err)));
});
},
updatePercent: (String object, double percent) {
emit(state.copyWith(object: object, percent: percent));
},
downloadFinish: () {
emit(state.copyWith(isFinish: true));
},
);
}
}
@freezed
class DownloadModelEvent with _$DownloadModelEvent {
const factory DownloadModelEvent.started() = _Started;
const factory DownloadModelEvent.updatePercent(
String object,
double percent,
) = _UpdatePercent;
const factory DownloadModelEvent.downloadFinish() = _DownloadFinish;
}
@freezed
class DownloadModelState with _$DownloadModelState {
const factory DownloadModelState({
required LLMModelPB model,
DownloadingStream? downloadStream,
String? downloadError,
@Default("") String object,
@Default(0) double percent,
@Default(false) bool isFinish,
@Default(LoadingState.loading()) LoadingState loadingState,
}) = _DownloadModelState;
}
class DownloadingStream {
DownloadingStream() {
_port.handler = _controller.add;
}
final RawReceivePort _port = RawReceivePort();
StreamSubscription<String>? _sub;
final StreamController<String> _controller = StreamController.broadcast();
int get nativePort => _port.sendPort.nativePort;
Future<void> dispose() async {
await _sub?.cancel();
await _controller.close();
_port.close();
}
void listen({
void Function(String modelName, double percent)? onModelPercentage,
void Function(double percent)? onPluginPercentage,
void Function(String data)? onError,
void Function()? onFinish,
}) {
_sub = _controller.stream.listen((text) {
if (text.contains(':progress:')) {
final progressIndex = text.indexOf(':progress:');
final modelName = text.substring(0, progressIndex);
final progressValue = text
.substring(progressIndex + 10); // 10 is the length of ":progress:"
final percent = double.tryParse(progressValue);
if (percent != null) {
onModelPercentage?.call(modelName, percent);
}
} else if (text.startsWith('plugin:progress:')) {
final percent = double.tryParse(text.substring(16));
if (percent != null) {
onPluginPercentage?.call(percent);
}
} else if (text.startsWith('finish')) {
onFinish?.call();
} else if (text.startsWith('error:')) {
// substring 6 to remove "error:"
onError?.call(text.substring(6));
}
});
}
}
@@ -0,0 +1,253 @@
import 'dart:async';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'local_ai_bloc.freezed.dart';
class LocalAISettingBloc
extends Bloc<LocalAISettingEvent, LocalAISettingState> {
LocalAISettingBloc()
: listener = LocalLLMListener(),
super(const LocalAISettingState()) {
listener.start(
stateCallback: (newState) {
if (!isClosed) {
add(LocalAISettingEvent.updateLLMRunningState(newState.state));
}
},
);
on<LocalAISettingEvent>(_handleEvent);
}
final LocalLLMListener listener;
/// Handles incoming events and dispatches them to the appropriate handler.
Future<void> _handleEvent(
LocalAISettingEvent event,
Emitter<LocalAISettingState> emit,
) async {
await event.when(
started: _handleStarted,
didLoadModelInfo: (FlowyResult<LLMModelInfoPB, FlowyError> result) {
result.fold(
(modelInfo) {
_fetchCurremtLLMState();
emit(
state.copyWith(
modelInfo: modelInfo,
models: modelInfo.models,
selectedLLMModel: modelInfo.selectedModel,
fetchModelInfoState: const LoadingState.finish(),
),
);
},
(err) {
emit(
state.copyWith(
fetchModelInfoState: LoadingState.finish(error: err),
),
);
},
);
},
selectLLMConfig: (LLMModelPB llmModel) async {
final result = await ChatEventUpdateLocalLLM(llmModel).send();
result.fold(
(llmResource) {
// If all resources are downloaded, show reload plugin
if (llmResource.pendingResources.isNotEmpty) {
emit(
state.copyWith(
selectedLLMModel: llmModel,
localAIInfo: LocalAIProgress.showDownload(
llmResource,
llmModel,
),
selectLLMState: const LoadingState.finish(),
),
);
} else {
emit(
state.copyWith(
selectedLLMModel: llmModel,
selectLLMState: const LoadingState.finish(),
localAIInfo: const LocalAIProgress.checkPluginState(),
),
);
}
},
(err) {
emit(
state.copyWith(
selectLLMState: LoadingState.finish(error: err),
),
);
},
);
},
refreshLLMState: (LocalModelResourcePB llmResource) {
if (state.selectedLLMModel == null) {
Log.error(
'Unexpected null selected config. It should be set already',
);
return;
}
// reload plugin if all resources are downloaded
if (llmResource.pendingResources.isEmpty) {
emit(
state.copyWith(
localAIInfo: const LocalAIProgress.checkPluginState(),
),
);
} else {
if (state.selectedLLMModel != null) {
// Go to download page if the selected model is downloading
if (llmResource.isDownloading) {
emit(
state.copyWith(
localAIInfo:
LocalAIProgress.startDownloading(state.selectedLLMModel!),
selectLLMState: const LoadingState.finish(),
),
);
return;
} else {
emit(
state.copyWith(
localAIInfo: LocalAIProgress.showDownload(
llmResource,
state.selectedLLMModel!,
),
selectLLMState: const LoadingState.finish(),
),
);
}
}
}
},
startDownloadModel: (LLMModelPB llmModel) {
emit(
state.copyWith(
localAIInfo: LocalAIProgress.startDownloading(llmModel),
selectLLMState: const LoadingState.finish(),
),
);
},
cancelDownload: () async {
final _ = await ChatEventCancelDownloadLLMResource().send();
_fetchCurremtLLMState();
},
finishDownload: () async {
emit(
state.copyWith(localAIInfo: const LocalAIProgress.finishDownload()),
);
},
updateLLMRunningState: (RunningStatePB newRunningState) {
if (newRunningState == RunningStatePB.Stopped) {
emit(
state.copyWith(
runningState: newRunningState,
localAIInfo: const LocalAIProgress.checkPluginState(),
),
);
} else {
emit(state.copyWith(runningState: newRunningState));
}
},
);
}
void _fetchCurremtLLMState() async {
final result = await ChatEventGetLocalLLMState().send();
result.fold(
(llmResource) {
if (!isClosed) {
add(LocalAISettingEvent.refreshLLMState(llmResource));
}
},
(err) {
Log.error(err);
},
);
}
/// Handles the event to fetch local AI settings when the application starts.
Future<void> _handleStarted() async {
final result = await ChatEventRefreshLocalAIModelInfo().send();
if (!isClosed) {
add(LocalAISettingEvent.didLoadModelInfo(result));
}
}
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
}
@freezed
class LocalAISettingEvent with _$LocalAISettingEvent {
const factory LocalAISettingEvent.started() = _Started;
const factory LocalAISettingEvent.didLoadModelInfo(
FlowyResult<LLMModelInfoPB, FlowyError> result,
) = _ModelInfo;
const factory LocalAISettingEvent.selectLLMConfig(LLMModelPB config) =
_SelectLLMConfig;
const factory LocalAISettingEvent.refreshLLMState(
LocalModelResourcePB llmResource,
) = _RefreshLLMResource;
const factory LocalAISettingEvent.startDownloadModel(LLMModelPB llmModel) =
_StartDownloadModel;
const factory LocalAISettingEvent.cancelDownload() = _CancelDownload;
const factory LocalAISettingEvent.finishDownload() = _FinishDownload;
const factory LocalAISettingEvent.updateLLMRunningState(
RunningStatePB newRunningState,
) = _RunningState;
}
@freezed
class LocalAISettingState with _$LocalAISettingState {
const factory LocalAISettingState({
LLMModelInfoPB? modelInfo,
LLMModelPB? selectedLLMModel,
LocalAIProgress? localAIInfo,
@Default(LoadingState.loading()) LoadingState fetchModelInfoState,
@Default(LoadingState.loading()) LoadingState selectLLMState,
@Default([]) List<LLMModelPB> models,
@Default(RunningStatePB.Connecting) RunningStatePB runningState,
}) = _LocalAISettingState;
}
@freezed
class LocalAIProgress with _$LocalAIProgress {
// when user select a new model, it will call requestDownload
const factory LocalAIProgress.requestDownloadInfo(
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) = _RequestDownload;
// when user comes back to the setting page, it will auto detect current llm state
const factory LocalAIProgress.showDownload(
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) = _DownloadNeeded;
// when start downloading the model
const factory LocalAIProgress.startDownloading(LLMModelPB llmModel) =
_Downloading;
const factory LocalAIProgress.finishDownload() = _Finish;
const factory LocalAIProgress.checkPluginState() = _PluginState;
}
@@ -0,0 +1,54 @@
import 'dart:async';
import 'dart:typed_data';
import 'package:appflowy/plugins/ai_chat/application/chat_notification.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/notification.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
import 'package:appflowy_backend/rust_stream.dart';
import 'package:appflowy_result/appflowy_result.dart';
typedef PluginStateCallback = void Function(PluginStatePB state);
class LocalLLMListener {
LocalLLMListener() {
_parser =
ChatNotificationParser(id: "appflowy_chat_plugin", callback: _callback);
_subscription = RustStreamReceiver.listen(
(observable) => _parser?.parse(observable),
);
}
StreamSubscription<SubscribeObject>? _subscription;
ChatNotificationParser? _parser;
PluginStateCallback? stateCallback;
void Function()? finishStreamingCallback;
void start({
PluginStateCallback? stateCallback,
}) {
this.stateCallback = stateCallback;
}
void _callback(
ChatNotification ty,
FlowyResult<Uint8List, FlowyError> result,
) {
result.map((r) {
switch (ty) {
case ChatNotification.UpdateChatPluginState:
stateCallback?.call(PluginStatePB.fromBuffer(r));
break;
default:
break;
}
});
}
Future<void> stop() async {
await _subscription?.cancel();
_subscription = null;
}
}
@@ -0,0 +1,88 @@
import 'dart:async';
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'plugin_state_bloc.freezed.dart';
class PluginStateBloc extends Bloc<PluginStateEvent, PluginStateState> {
PluginStateBloc()
: listener = LocalLLMListener(),
super(const PluginStateState(action: PluginStateAction.init())) {
listener.start(
stateCallback: (pluginState) {
if (!isClosed) {
add(PluginStateEvent.updateState(pluginState));
}
},
);
on<PluginStateEvent>(_handleEvent);
}
final LocalLLMListener listener;
@override
Future<void> close() async {
await listener.stop();
return super.close();
}
Future<void> _handleEvent(
PluginStateEvent event,
Emitter<PluginStateState> emit,
) async {
await event.when(
started: () async {
final result = await ChatEventGetPluginState().send();
result.fold(
(pluginState) {
if (!isClosed) {
add(PluginStateEvent.updateState(pluginState));
}
},
(err) => Log.error(err.toString()),
);
},
updateState: (PluginStatePB pluginState) {
switch (pluginState.state) {
case RunningStatePB.Running:
emit(const PluginStateState(action: PluginStateAction.ready()));
break;
default:
emit(
state.copyWith(action: const PluginStateAction.reloadRequired()),
);
break;
}
},
restartLocalAI: () {
ChatEventRestartLocalAI().send();
},
);
}
}
@freezed
class PluginStateEvent with _$PluginStateEvent {
const factory PluginStateEvent.started() = _Started;
const factory PluginStateEvent.updateState(PluginStatePB pluginState) =
_UpdatePluginState;
const factory PluginStateEvent.restartLocalAI() = _RestartLocalAI;
}
@freezed
class PluginStateState with _$PluginStateState {
const factory PluginStateState({required PluginStateAction action}) =
_PluginStateState;
}
@freezed
class PluginStateAction with _$PluginStateAction {
const factory PluginStateAction.init() = _Init;
const factory PluginStateAction.ready() = _Ready;
const factory PluginStateAction.reloadRequired() = _ReloadRequired;
}
@@ -1,195 +0,0 @@
import 'dart:io';
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:bloc/bloc.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:path/path.dart' as path;
import 'package:protobuf/protobuf.dart';
part 'setting_local_ai_bloc.freezed.dart';
class SettingsAILocalBloc
extends Bloc<SettingsAILocalEvent, SettingsAILocalState> {
SettingsAILocalBloc() : super(const SettingsAILocalState()) {
on<SettingsAILocalEvent>(_handleEvent);
}
/// Handles incoming events and dispatches them to the appropriate handler.
Future<void> _handleEvent(
SettingsAILocalEvent event,
Emitter<SettingsAILocalState> emit,
) async {
await event.when(
started: _handleStarted,
didUpdateAISetting: (settings) async {
_handleDidUpdateAISetting(settings, emit);
},
updateChatBin: (chatBinPath) async {
await _handleUpdatePath(
filePath: chatBinPath,
emit: emit,
stateUpdater: () => state.copyWith(
chatBinPath: chatBinPath.trim(),
chatBinPathError: null,
),
errorUpdater: (error) => state.copyWith(chatBinPathError: error),
);
},
updateChatModelPath: (chatModelPath) async {
await _handleUpdatePath(
filePath: chatModelPath,
emit: emit,
stateUpdater: () => state.copyWith(
chatModelPath: chatModelPath.trim(),
chatModelPathError: null,
),
errorUpdater: (error) => state.copyWith(chatModelPathError: error),
);
},
toggleLocalAI: () async {
emit(state.copyWith(localAIEnabled: !state.localAIEnabled));
},
saveSetting: () async {
_handleSaveSetting();
},
);
}
/// Handles the event to fetch local AI settings when the application starts.
Future<void> _handleStarted() async {
final result = await ChatEventGetLocalAISetting().send();
result.fold(
(setting) {
if (!isClosed) {
add(SettingsAILocalEvent.didUpdateAISetting(setting));
}
},
(err) => Log.error('Failed to get local AI setting: $err'),
);
}
/// Handles the event to update the AI settings in the state.
void _handleDidUpdateAISetting(
LocalLLMSettingPB settings,
Emitter<SettingsAILocalState> emit,
) {
final newState = state.copyWith(
aiSettings: settings,
chatBinPath: settings.chatBinPath,
chatModelPath: settings.chatModelPath,
localAIEnabled: settings.enabled,
loadingState: const LoadingState.finish(),
);
emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState)));
}
/// Handles updating file paths (both chat binary and chat model paths).
Future<void> _handleUpdatePath({
required String filePath,
required Emitter<SettingsAILocalState> emit,
required SettingsAILocalState Function() stateUpdater,
required SettingsAILocalState Function(String) errorUpdater,
}) async {
filePath = filePath.trim();
if (filePath.isEmpty) {
emit(stateUpdater());
return;
}
final validationError = await _validatePath(filePath);
if (validationError != null) {
emit(errorUpdater(validationError));
return;
}
final newState = stateUpdater();
emit(newState.copyWith(saveButtonEnabled: _saveButtonEnabled(newState)));
}
/// Validates the provided file path.
Future<String?> _validatePath(String filePath) async {
if (!isAbsolutePath(filePath)) {
return "$filePath must be absolute";
}
if (!await pathExists(filePath)) {
return "$filePath does not exist";
}
return null;
}
/// Handles saving the updated settings.
void _handleSaveSetting() {
if (state.aiSettings == null) return;
state.aiSettings!.freeze();
final newSetting = state.aiSettings!.rebuild((value) {
value
..chatBinPath = state.chatBinPath ?? value.chatBinPath
..chatModelPath = state.chatModelPath ?? value.chatModelPath
..enabled = state.localAIEnabled;
});
ChatEventUpdateLocalAISetting(newSetting).send().then((result) {
result.fold(
(_) {
if (!isClosed) {
add(SettingsAILocalEvent.didUpdateAISetting(newSetting));
}
},
(err) => Log.error('Failed to update local AI setting: $err'),
);
});
}
/// Determines if the save button should be enabled based on the state.
bool _saveButtonEnabled(SettingsAILocalState newState) {
return newState.chatBinPathError == null &&
newState.chatModelPathError == null &&
newState.chatBinPath != null &&
newState.chatModelPath != null;
}
}
@freezed
class SettingsAILocalEvent with _$SettingsAILocalEvent {
const factory SettingsAILocalEvent.started() = _Started;
const factory SettingsAILocalEvent.didUpdateAISetting(
LocalLLMSettingPB settings,
) = _GetAISetting;
const factory SettingsAILocalEvent.updateChatBin(String chatBinPath) =
_UpdateChatBin;
const factory SettingsAILocalEvent.updateChatModelPath(String chatModelPath) =
_UpdateChatModelPath;
const factory SettingsAILocalEvent.toggleLocalAI() = _EnableLocalAI;
const factory SettingsAILocalEvent.saveSetting() = _SaveSetting;
}
@freezed
class SettingsAILocalState with _$SettingsAILocalState {
const factory SettingsAILocalState({
LocalLLMSettingPB? aiSettings,
String? chatBinPath,
String? chatBinPathError,
String? chatModelPath,
String? chatModelPathError,
@Default(false) bool localAIEnabled,
@Default(false) bool saveButtonEnabled,
@Default(LoadingState.loading()) LoadingState loadingState,
}) = _SettingsAILocalState;
}
/// Checks if a given file path is absolute.
bool isAbsolutePath(String filePath) {
return path.isAbsolute(filePath);
}
/// Checks if a given file or directory path exists.
Future<bool> pathExists(String filePath) async {
final file = File(filePath);
final directory = Directory(filePath);
return await file.exists() || await directory.exists();
}
@@ -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,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
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
@@ -8,6 +6,7 @@ import 'package:appflowy/workspace/presentation/command_palette/widgets/recent_v
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class RecentViewsList extends StatelessWidget {
@@ -24,7 +23,7 @@ class RecentViewsList extends StatelessWidget {
builder: (context, state) {
// We remove duplicates by converting the list to a set first
final List<ViewPB> recentViews =
state.views.reversed.map((e) => e.item).toSet().toList();
state.views.map((e) => e.item).toSet().toList();
return ListView.separated(
shrinkWrap: true,
@@ -4,11 +4,11 @@ import 'dart:io';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
import 'package:appflowy/shared/markdown_to_document.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/share/import_service.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/import/import_type.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/file_picker/file_picker_service.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -220,7 +220,7 @@ class _ImportPanelState extends State<ImportPanel> {
Uint8List? _documentDataFrom(ImportType importType, String data) {
switch (importType) {
case ImportType.markdownOrText:
final document = markdownToDocument(data);
final document = customMarkdownToDocument(data);
return DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
case ImportType.historyDocument:
final document = EditorMigration.migrateDocument(data);
@@ -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),
),
),
),
@@ -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),
@@ -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 {
@@ -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));
},
);
}
@@ -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(),
@@ -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,
@@ -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));
},
);
},
@@ -7,6 +7,7 @@ import 'package:appflowy/workspace/presentation/settings/shared/single_setting_a
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_result/appflowy_result.dart';
import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -24,7 +25,7 @@ class FixDataWidget extends StatelessWidget {
.tr(),
buttonLabel: LocaleKeys.settings_manageDataPage_data_fixButton.tr(),
onPressed: () {
FixDataManager.checkWorkspaceHealth(dryRun: true);
WorkspaceDataManager.checkWorkspaceHealth(dryRun: true);
},
),
],
@@ -32,7 +33,7 @@ class FixDataWidget extends StatelessWidget {
}
}
class FixDataManager {
class WorkspaceDataManager {
static Future<void> checkWorkspaceHealth({
required bool dryRun,
}) async {
@@ -63,6 +64,9 @@ class FixDataManager {
// check the health of the spaces
await checkSpaceHealth(workspace: workspace, allViews: allViews);
// check the health of the views
await checkViewHealth(workspace: workspace, allViews: allViews);
// add other checks here
// ...
} catch (e) {
@@ -83,7 +87,6 @@ class FixDataManager {
workspaceChildViews.map((e) => e.id).toSet();
final spaces = allViews.where((e) => e.isSpace).toList();
//
for (final space in spaces) {
// the space is the top level view, so its parent view id should be the workspace id
// and the workspace should have the space in its child views
@@ -106,6 +109,93 @@ class FixDataManager {
}
}
static Future<List<ViewPB>> checkViewHealth({
ViewPB? workspace,
List<ViewPB>? allViews,
bool dryRun = true,
}) async {
// Views whose parent view does not have the view in its child views
final List<ViewPB> unlistedChildViews = [];
// Views whose parent is not in allViews
final List<ViewPB> orphanViews = [];
try {
if (workspace == null || allViews == null) {
final currentWorkspace =
await UserBackendService.getCurrentWorkspace().getOrThrow();
// get all the views in the workspace
final result = await ViewBackendService.getAllViews().getOrThrow();
allViews = result.items;
workspace = allViews.firstWhereOrNull(
(e) => e.id == currentWorkspace.id,
);
}
for (final view in allViews) {
if (view.parentViewId == '') {
continue;
}
final parentView = allViews.firstWhereOrNull(
(e) => e.id == view.parentViewId,
);
if (parentView == null) {
orphanViews.add(view);
continue;
}
final childViewsOfParent =
await ViewBackendService.getChildViews(viewId: parentView.id)
.getOrThrow();
final result = childViewsOfParent.any((e) => e.id == view.id);
if (!result) {
unlistedChildViews.add(view);
}
}
} catch (e) {
Log.error('Failed to check space health: $e');
return [];
}
for (final view in unlistedChildViews) {
Log.info(
'[workspace] found an issue: view is not in the parent view\'s child views, view: ${view.toProto3Json()}}',
);
}
for (final view in orphanViews) {
Log.debug('[workspace] orphanViews: ${view.toProto3Json()}');
}
if (!dryRun && unlistedChildViews.isNotEmpty) {
Log.info(
'[workspace] start to fix ${unlistedChildViews.length} unlistedChildViews ...',
);
for (final view in unlistedChildViews) {
// move the view to the parent view if it is not in the parent view's child views
Log.info(
'[workspace] move view: $view to its parent view ${view.parentViewId}',
);
await ViewBackendService.moveViewV2(
viewId: view.id,
newParentId: view.parentViewId,
prevViewId: null,
);
}
Log.info('[workspace] end to fix unlistedChildViews');
}
if (unlistedChildViews.isEmpty && orphanViews.isEmpty) {
Log.info('[workspace] all views are healthy');
}
Log.info('[workspace] done checking view health');
return unlistedChildViews;
}
static void dumpViews(String prefix, List<ViewPB> views) {
for (int i = 0; i < views.length; i++) {
final view = views[i];
@@ -0,0 +1,108 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/download_model_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:percent_indicator/linear_percent_indicator.dart';
class DownloadingIndicator extends StatelessWidget {
const DownloadingIndicator({
required this.llmModel,
required this.onCancel,
required this.onFinish,
super.key,
});
final LLMModelPB llmModel;
final VoidCallback onCancel;
final VoidCallback onFinish;
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
DownloadModelBloc(llmModel)..add(const DownloadModelEvent.started()),
child: BlocListener<DownloadModelBloc, DownloadModelState>(
listener: (context, state) {
if (state.isFinish) {
onFinish();
}
},
child: DecoratedBox(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(8),
),
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Column(
children: [
// const DownloadingPrompt(),
// const VSpace(12),
DownloadingProgressBar(onCancel: onCancel),
],
),
),
),
),
);
}
}
class DownloadingProgressBar extends StatelessWidget {
const DownloadingProgressBar({required this.onCancel, super.key});
final VoidCallback onCancel;
@override
Widget build(BuildContext context) {
return BlocBuilder<DownloadModelBloc, DownloadModelState>(
builder: (context, state) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
"${LocaleKeys.settings_aiPage_keys_downloadingModel.tr()}: ${state.object}",
fontSize: 11,
),
IntrinsicHeight(
child: Row(
children: [
Expanded(
child: LinearPercentIndicator(
lineHeight: 9.0,
percent: state.percent,
padding: EdgeInsets.zero,
progressColor: AFThemeExtension.of(context).success,
backgroundColor:
AFThemeExtension.of(context).progressBarBGColor,
barRadius: const Radius.circular(8),
trailing: FlowyText(
"${(state.percent * 100).toStringAsFixed(0)}%",
fontSize: 11,
color: AFThemeExtension.of(context).success,
),
),
),
const HSpace(12),
FlowyButton(
useIntrinsicWidth: true,
text: FlowyText(
LocaleKeys.button_cancel.tr(),
fontSize: 11,
),
onTap: onCancel,
),
],
),
),
],
);
},
);
}
}
@@ -0,0 +1,75 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class InitLocalAIIndicator extends StatelessWidget {
const InitLocalAIIndicator({super.key});
@override
Widget build(BuildContext context) {
return DecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFFEDF7ED),
borderRadius: BorderRadius.all(
Radius.circular(4),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
switch (state.runningState) {
case RunningStatePB.Connecting:
case RunningStatePB.Connected:
return Row(
children: [
const HSpace(8),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoading.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
],
);
case RunningStatePB.Running:
return Row(
children: [
const HSpace(8),
const FlowySvg(
FlowySvgs.download_success_s,
color: Color(0xFF2E7D32),
),
const HSpace(6),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoaded.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
],
);
case RunningStatePB.Stopped:
return Row(
children: [
const HSpace(8),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAIStopped.tr(),
fontSize: 11,
color: const Color(0xFFC62828),
),
],
);
default:
return const SizedBox.shrink();
}
},
),
),
);
}
}
@@ -0,0 +1,314 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/workspace/application/settings/ai/local_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/downloading.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/init_local_ai.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/plugin_state.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class LocalModelConfig extends StatelessWidget {
const LocalModelConfig({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
if (state.aiSettings == null) {
return const SizedBox.shrink();
}
if (state.aiSettings!.aiModel != AIModelPB.LocalAIModel) {
return const SizedBox.shrink();
}
return BlocProvider(
create: (context) =>
LocalAISettingBloc()..add(const LocalAISettingEvent.started()),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModel.tr(),
fontSize: 14,
),
),
const Spacer(),
BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
return state.fetchModelInfoState.when(
loading: () =>
const CircularProgressIndicator.adaptive(),
finish: (err) {
return (err == null)
? const _SelectLocalModelDropdownMenu()
: const SizedBox.shrink();
},
);
},
),
],
),
const IntrinsicHeight(child: _LocalLLMInfoWidget()),
],
),
),
);
},
);
}
}
class _SelectLocalModelDropdownMenu extends StatelessWidget {
const _SelectLocalModelDropdownMenu();
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
return Flexible(
child: SettingsDropdown<LLMModelPB>(
key: const Key('_SelectLocalModelDropdownMenu'),
onChanged: (model) => context.read<LocalAISettingBloc>().add(
LocalAISettingEvent.selectLLMConfig(model),
),
selectedOption: state.selectedLLMModel!,
options: state.models
.map(
(llm) => buildDropdownMenuEntry<LLMModelPB>(
context,
value: llm,
label: llm.chatModel,
padding: const EdgeInsets.symmetric(vertical: 8),
),
)
.toList(),
),
);
},
);
}
}
class _LocalLLMInfoWidget extends StatelessWidget {
const _LocalLLMInfoWidget();
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
final error = errorFromState(state);
if (error == null) {
// If the error is null, handle selected llm model.
if (state.localAIInfo != null) {
final child = state.localAIInfo!.when(
requestDownloadInfo: (
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) {
_showDownloadDialog(context, llmResource, llmModel);
return const SizedBox.shrink();
},
showDownload: (
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) =>
_ShowDownloadIndicator(
llmResource: llmResource,
llmModel: llmModel,
),
startDownloading: (llmModel) {
return DownloadingIndicator(
key: UniqueKey(),
llmModel: llmModel,
onFinish: () => context
.read<LocalAISettingBloc>()
.add(const LocalAISettingEvent.finishDownload()),
onCancel: () => context
.read<LocalAISettingBloc>()
.add(const LocalAISettingEvent.cancelDownload()),
);
},
finishDownload: () => const InitLocalAIIndicator(),
checkPluginState: () => const CheckPluginStateIndicator(),
);
return Padding(
padding: const EdgeInsets.only(top: 14),
child: child,
);
} else {
return const SizedBox.shrink();
}
} else {
return FlowyText(
error.msg,
maxLines: 10,
);
}
},
);
}
void _showDownloadDialog(
BuildContext context,
LocalModelResourcePB llmResource,
LLMModelPB llmModel,
) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) {
return _LLMModelDownloadDialog(
llmResource: llmResource,
onOkPressed: () {
context.read<LocalAISettingBloc>().add(
LocalAISettingEvent.startDownloadModel(
llmModel,
),
);
},
onCancelPressed: () {
context.read<LocalAISettingBloc>().add(
const LocalAISettingEvent.cancelDownload(),
);
},
);
},
);
},
debugLabel: 'localModel.download',
);
}
FlowyError? errorFromState(LocalAISettingState state) {
final err = state.fetchModelInfoState.when(
loading: () => null,
finish: (err) => err,
);
if (err == null) {
state.selectLLMState.when(
loading: () => null,
finish: (err) => err,
);
}
return err;
}
}
class _LLMModelDownloadDialog extends StatelessWidget {
const _LLMModelDownloadDialog({
required this.llmResource,
required this.onOkPressed,
required this.onCancelPressed,
});
final LocalModelResourcePB llmResource;
final VoidCallback onOkPressed;
final VoidCallback onCancelPressed;
@override
Widget build(BuildContext context) {
return NavigatorOkCancelDialog(
title: LocaleKeys.settings_aiPage_keys_downloadLLMPrompt.tr(
args: [
llmResource.pendingResources[0].name,
],
),
message: llmResource.pendingResources[0].fileSize == 0
? ""
: LocaleKeys.settings_aiPage_keys_downloadLLMPromptDetail.tr(
args: [
llmResource.pendingResources[0].name,
llmResource.pendingResources[0].fileSize.toString(),
],
),
okTitle: LocaleKeys.button_confirm.tr(),
cancelTitle: LocaleKeys.button_cancel.tr(),
onOkPressed: onOkPressed,
onCancelPressed: onCancelPressed,
titleUpperCase: false,
);
}
}
class _ShowDownloadIndicator extends StatelessWidget {
const _ShowDownloadIndicator({
required this.llmResource,
required this.llmModel,
});
final LocalModelResourcePB llmResource;
final LLMModelPB llmModel;
@override
Widget build(BuildContext context) {
return BlocBuilder<LocalAISettingBloc, LocalAISettingState>(
builder: (context, state) {
return Row(
children: [
const Spacer(),
IntrinsicWidth(
child: SizedBox(
height: 30,
child: FlowyButton(
text: FlowyText(
LocaleKeys.settings_aiPage_keys_downloadAIModelButton.tr(),
fontSize: 14,
color: const Color(0xFF005483),
),
leftIcon: const FlowySvg(
FlowySvgs.local_model_download_s,
color: Color(0xFF005483),
),
onTap: () {
showDialog(
context: context,
barrierDismissible: false,
useRootNavigator: false,
builder: (dialogContext) {
return _LLMModelDownloadDialog(
llmResource: llmResource,
onOkPressed: () {
context.read<LocalAISettingBloc>().add(
LocalAISettingEvent.startDownloadModel(
llmModel,
),
);
},
onCancelPressed: () {
context.read<LocalAISettingBloc>().add(
const LocalAISettingEvent.cancelDownload(),
);
},
);
},
);
},
),
),
),
],
);
},
);
}
}
@@ -0,0 +1,85 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIModelSelection extends StatelessWidget {
const AIModelSelection({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModelType.tr(),
fontSize: 14,
),
),
const Spacer(),
Flexible(
child: SettingsDropdown<AIModelPB>(
key: const Key('_AIModelSelection'),
onChanged: (model) => context
.read<SettingsAIBloc>()
.add(SettingsAIEvent.selectModel(model)),
selectedOption: state.userProfile.aiModel,
options: _availableModels
.map(
(format) => buildDropdownMenuEntry<AIModelPB>(
context,
value: format,
label: _titleForAIModel(format),
),
)
.toList(),
),
),
],
),
);
},
);
}
}
List<AIModelPB> _availableModels = [
AIModelPB.DefaultModel,
AIModelPB.Claude3Opus,
AIModelPB.Claude3Sonnet,
AIModelPB.GPT35,
AIModelPB.GPT4o,
// AIModelPB.LocalAIModel,
];
String _titleForAIModel(AIModelPB model) {
switch (model) {
case AIModelPB.DefaultModel:
return "Default";
case AIModelPB.Claude3Opus:
return "Claude 3 Opus";
case AIModelPB.Claude3Sonnet:
return "Claude 3 Sonnet";
case AIModelPB.GPT35:
return "GPT-3.5";
case AIModelPB.GPT4o:
return "GPT-4o";
case AIModelPB.LocalAIModel:
return "Local";
default:
Log.error("Unknown AI model: $model, fallback to default");
return "Default";
}
}
@@ -0,0 +1,111 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/plugin_state_bloc.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class CheckPluginStateIndicator extends StatelessWidget {
const CheckPluginStateIndicator({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
PluginStateBloc()..add(const PluginStateEvent.started()),
child: BlocBuilder<PluginStateBloc, PluginStateState>(
builder: (context, state) {
return state.action.when(
init: () => const _InitPlugin(),
ready: () => const _ReadyToUse(),
reloadRequired: () => const _ReloadButton(),
);
},
),
);
}
}
class _InitPlugin extends StatelessWidget {
const _InitPlugin();
@override
Widget build(BuildContext context) {
return const SizedBox(
height: 20,
child: CircularProgressIndicator.adaptive(),
);
}
}
class _ReloadButton extends StatelessWidget {
const _ReloadButton();
@override
Widget build(BuildContext context) {
return Row(
children: [
const FlowySvg(
FlowySvgs.download_warn_s,
color: Color(0xFFC62828),
),
const HSpace(6),
FlowyText(LocaleKeys.settings_aiPage_keys_failToLoadLocalAI.tr()),
const Spacer(),
SizedBox(
height: 30,
child: FlowyButton(
useIntrinsicWidth: true,
text:
FlowyText(LocaleKeys.settings_aiPage_keys_restartLocalAI.tr()),
onTap: () {
context.read<PluginStateBloc>().add(
const PluginStateEvent.restartLocalAI(),
);
},
),
),
],
);
}
}
class _ReadyToUse extends StatelessWidget {
const _ReadyToUse();
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: DecoratedBox(
decoration: const BoxDecoration(
color: Color(0xFFEDF7ED),
borderRadius: BorderRadius.all(
Radius.circular(4),
),
),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
const HSpace(8),
const FlowySvg(
FlowySvgs.download_success_s,
color: Color(0xFF2E7D32),
),
const HSpace(6),
FlowyText(
LocaleKeys.settings_aiPage_keys_localAILoaded.tr(),
fontSize: 11,
color: const Color(0xFF1E4620),
),
],
),
),
),
);
}
}
@@ -0,0 +1,107 @@
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/local_ai_config.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/model_selection.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget {
const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30),
child: FlowyText(
LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(),
maxLines: null,
fontSize: 16,
lineHeight: 1.6,
),
);
}
}
class SettingsAIView extends StatelessWidget {
const SettingsAIView({super.key, required this.userProfile});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsAIBloc>(
create: (_) =>
SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
final children = <Widget>[
const AIModelSelection(),
];
if (state.aiSettings != null &&
state.aiSettings!.aiModel == AIModelPB.LocalAIModel) {
children.add(const LocalModelConfig());
}
children.add(const _AISearchToggle(value: false));
return SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
description:
LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(),
children: children,
);
},
),
);
}
}
class _AISearchToggle extends StatelessWidget {
const _AISearchToggle({required this.value});
final bool value;
@override
Widget build(BuildContext context) {
return Column(
children: [
Row(
children: [
FlowyText.medium(
LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(),
),
const Spacer(),
BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
if (state.aiSettings == null) {
return const Padding(
padding: EdgeInsets.only(top: 6),
child: SizedBox(
height: 26,
width: 26,
child: CircularProgressIndicator.adaptive(),
),
);
} else {
return Toggle(
value: state.enableSearchIndexing,
onChanged: (_) => context
.read<SettingsAIBloc>()
.add(const SettingsAIEvent.toggleAISearch()),
);
}
},
),
],
),
],
);
}
}
@@ -1,279 +0,0 @@
import 'package:appflowy/workspace/application/settings/ai/setting_local_ai_bloc.dart';
import 'package:flowy_infra_ui/style_widget/button.dart';
import 'package:flowy_infra_ui/widget/rounded_input_field.dart';
import 'package:flowy_infra_ui/widget/spacing.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/ai/settings_ai_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_dropdown.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/style_widget/text.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class AIFeatureOnlySupportedWhenUsingAppFlowyCloud extends StatelessWidget {
const AIFeatureOnlySupportedWhenUsingAppFlowyCloud({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 30),
child: FlowyText(
LocaleKeys.settings_aiPage_keys_loginToEnableAIFeature.tr(),
maxLines: null,
fontSize: 16,
lineHeight: 1.6,
),
);
}
}
class SettingsAIView extends StatelessWidget {
const SettingsAIView({super.key, required this.userProfile});
final UserProfilePB userProfile;
@override
Widget build(BuildContext context) {
return BlocProvider<SettingsAIBloc>(
create: (_) =>
SettingsAIBloc(userProfile)..add(const SettingsAIEvent.started()),
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
return SettingsBody(
title: LocaleKeys.settings_aiPage_title.tr(),
description:
LocaleKeys.settings_aiPage_keys_aiSettingsDescription.tr(),
children: const [
AIModelSelection(),
_AISearchToggle(value: false),
// Disable local AI configuration for now. It's not ready for production.
// LocalAIConfiguration(),
],
);
},
),
);
}
}
class AIModelSelection extends StatelessWidget {
const AIModelSelection({super.key});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Flexible(
child: FlowyText.medium(
LocaleKeys.settings_aiPage_keys_llmModel.tr(),
),
),
const Spacer(),
Flexible(
child: BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
return SettingsDropdown<AIModelPB>(
key: const Key('AIModelDropdown'),
onChanged: (model) => context
.read<SettingsAIBloc>()
.add(SettingsAIEvent.selectModel(model)),
selectedOption: state.userProfile.aiModel,
options: _availableModels
.map(
(format) => buildDropdownMenuEntry<AIModelPB>(
context,
value: format,
label: _titleForAIModel(format),
),
)
.toList(),
);
},
),
),
],
);
}
}
List<AIModelPB> _availableModels = [
AIModelPB.DefaultModel,
AIModelPB.Claude3Opus,
AIModelPB.Claude3Sonnet,
AIModelPB.GPT35,
AIModelPB.GPT4o,
];
String _titleForAIModel(AIModelPB model) {
switch (model) {
case AIModelPB.DefaultModel:
return "Default";
case AIModelPB.Claude3Opus:
return "Claude 3 Opus";
case AIModelPB.Claude3Sonnet:
return "Claude 3 Sonnet";
case AIModelPB.GPT35:
return "GPT-3.5";
case AIModelPB.GPT4o:
return "GPT-4o";
case AIModelPB.LocalAIModel:
return "Local";
default:
Log.error("Unknown AI model: $model, fallback to default");
return "Default";
}
}
class _AISearchToggle extends StatelessWidget {
const _AISearchToggle({required this.value});
final bool value;
@override
Widget build(BuildContext context) {
return Row(
children: [
FlowyText.medium(
LocaleKeys.settings_aiPage_keys_enableAISearchTitle.tr(),
),
const Spacer(),
BlocBuilder<SettingsAIBloc, SettingsAIState>(
builder: (context, state) {
if (state.aiSettings == null) {
return const Padding(
padding: EdgeInsets.only(top: 6),
child: SizedBox(
height: 26,
width: 26,
child: CircularProgressIndicator.adaptive(),
),
);
} else {
return Toggle(
value: state.enableSearchIndexing,
onChanged: (_) => context
.read<SettingsAIBloc>()
.add(const SettingsAIEvent.toggleAISearch()),
);
}
},
),
],
);
}
}
class LocalAIConfiguration extends StatelessWidget {
const LocalAIConfiguration({super.key});
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) =>
SettingsAILocalBloc()..add(const SettingsAILocalEvent.started()),
child: BlocBuilder<SettingsAILocalBloc, SettingsAILocalState>(
builder: (context, state) {
return state.loadingState.when(
loading: () {
return const SizedBox.shrink();
},
finish: () {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
AIConfigurateTextField(
title: 'chat bin path',
hitText: '',
errorText: state.chatBinPathError ?? '',
value: state.aiSettings?.chatBinPath ?? '',
onChanged: (value) {
context.read<SettingsAILocalBloc>().add(
SettingsAILocalEvent.updateChatBin(value),
);
},
),
const VSpace(16),
AIConfigurateTextField(
title: 'chat model path',
hitText: '',
errorText: state.chatModelPathError ?? '',
value: state.aiSettings?.chatModelPath ?? '',
onChanged: (value) {
context.read<SettingsAILocalBloc>().add(
SettingsAILocalEvent.updateChatModelPath(value),
);
},
),
const VSpace(16),
Toggle(
value: state.localAIEnabled,
onChanged: (_) => context
.read<SettingsAILocalBloc>()
.add(const SettingsAILocalEvent.toggleLocalAI()),
),
const VSpace(16),
FlowyButton(
disable: !state.saveButtonEnabled,
text: const FlowyText("save"),
onTap: () {
context.read<SettingsAILocalBloc>().add(
const SettingsAILocalEvent.saveSetting(),
);
},
),
],
);
},
);
},
),
);
}
}
class AIConfigurateTextField extends StatelessWidget {
const AIConfigurateTextField({
required this.title,
required this.hitText,
required this.errorText,
required this.value,
required this.onChanged,
super.key,
});
final String title;
final String hitText;
final String errorText;
final String value;
final void Function(String) onChanged;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
FlowyText(
title,
),
const VSpace(8),
RoundedInputField(
hintText: hitText,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500),
normalBorderColor: Theme.of(context).colorScheme.outline,
errorBorderColor: Theme.of(context).colorScheme.error,
cursorColor: Theme.of(context).colorScheme.primary,
errorText: errorText,
initialValue: value,
onChanged: onChanged,
),
],
);
}
}
@@ -126,26 +126,34 @@ class SettingsManageDataView extends StatelessWidget {
buttonLabel:
LocaleKeys.settings_manageDataPage_cache_title.tr(),
onPressed: () {
SettingsAlertDialog(
showCancelAndConfirmDialog(
context: context,
title: LocaleKeys
.settings_manageDataPage_cache_dialog_title
.tr(),
subtitle: LocaleKeys
description: LocaleKeys
.settings_manageDataPage_cache_dialog_description
.tr(),
confirm: () async {
confirmLabel: LocaleKeys.button_ok.tr(),
onConfirm: () async {
// clear all cache
await getIt<FlowyCacheManager>().clearAllCache();
// check the workspace and space health
await WorkspaceDataManager.checkViewHealth(
dryRun: false,
);
if (context.mounted) {
showSnackBarMessage(
showToastNotification(
context,
LocaleKeys
message: LocaleKeys
.settings_manageDataPage_cache_dialog_successHint
.tr(),
);
Navigator.of(context).pop();
}
},
).show(context);
);
},
),
],
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/theme_extension.dart';
@@ -12,6 +10,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsPlanComparisonDialog extends StatefulWidget {
@@ -173,9 +172,10 @@ class _SettingsPlanComparisonDialogState
description: LocaleKeys
.settings_comparePlanDialog_freePlan_description
.tr(),
// TODO(Mathias): the price should be dynamic based on the country and currency
price: LocaleKeys
.settings_comparePlanDialog_freePlan_price
.tr(),
.tr(args: ['\$0']),
priceInfo: LocaleKeys
.settings_comparePlanDialog_freePlan_priceInfo
.tr(),
@@ -231,9 +231,10 @@ class _SettingsPlanComparisonDialogState
description: LocaleKeys
.settings_comparePlanDialog_proPlan_description
.tr(),
// TODO(Mathias): the price should be dynamic based on the country and currency
price: LocaleKeys
.settings_comparePlanDialog_proPlan_price
.tr(),
.tr(args: ['\$10 ']),
priceInfo: LocaleKeys
.settings_comparePlanDialog_proPlan_priceInfo
.tr(),
@@ -1,5 +1,3 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/int64_extension.dart';
@@ -19,6 +17,7 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class SettingsPlanView extends StatelessWidget {
@@ -710,7 +709,7 @@ class _PlanProgressIndicator extends StatelessWidget {
// children: [
// FlowyText.semibold(
// LocaleKeys.settings_planPage_planUsage_aiCredit_price
// .tr(),
// .tr(args: ['5\$]),
// fontSize: 24,
// ),
// FlowyText.medium(
@@ -15,6 +15,7 @@ import 'package:appflowy/workspace/application/settings/date_time/date_format_ex
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
import 'package:appflowy/workspace/application/settings/workspace/workspace_settings_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_icon.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/settings/shared/af_dropdown_menu_entry.dart';
@@ -31,6 +32,7 @@ import 'package:appflowy/workspace/presentation/settings/shared/settings_input_f
import 'package:appflowy/workspace/presentation/settings/shared/settings_radio_select.dart';
import 'package:appflowy/workspace/presentation/settings/shared/single_setting_action.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/theme_upload/theme_upload_view.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
@@ -44,7 +46,6 @@ import 'package:flowy_infra/theme.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/style_widget/hover.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:google_fonts/google_fonts.dart';
@@ -1144,9 +1145,21 @@ class _DocumentCursorColorSetting extends StatelessWidget {
return SettingListTile(
label: label,
resetButtonKey: const Key('DocumentCursorColorResetButton'),
onResetRequested: () => context
..read<AppearanceSettingsCubit>().resetDocumentCursorColor()
..read<DocumentAppearanceCubit>().syncCursorColor(null),
onResetRequested: () {
showConfirmDialog(
context: context,
title:
LocaleKeys.settings_workspacePage_resetCursorColor_title.tr(),
description: LocaleKeys
.settings_workspacePage_resetCursorColor_description
.tr(),
style: ConfirmPopupStyle.cancelAndOk,
confirmLabel: LocaleKeys.settings_common_reset.tr(),
onConfirm: () => context
..read<AppearanceSettingsCubit>().resetDocumentCursorColor()
..read<DocumentAppearanceCubit>().syncCursorColor(null),
);
},
trailing: [
DocumentColorSettingButton(
key: const Key('DocumentCursorColorSettingButton'),
@@ -1202,9 +1215,21 @@ class _DocumentSelectionColorSetting extends StatelessWidget {
return SettingListTile(
label: label,
resetButtonKey: const Key('DocumentSelectionColorResetButton'),
onResetRequested: () => context
..read<AppearanceSettingsCubit>().resetDocumentSelectionColor()
..read<DocumentAppearanceCubit>().syncSelectionColor(null),
onResetRequested: () {
showConfirmDialog(
context: context,
title: LocaleKeys.settings_workspacePage_resetSelectionColor_title
.tr(),
description: LocaleKeys
.settings_workspacePage_resetSelectionColor_description
.tr(),
style: ConfirmPopupStyle.cancelAndOk,
confirmLabel: LocaleKeys.settings_common_reset.tr(),
onConfirm: () => context
..read<AppearanceSettingsCubit>().resetDocumentSelectionColor()
..read<DocumentAppearanceCubit>().syncSelectionColor(null),
);
},
trailing: [
DocumentColorSettingButton(
currentColor: state.selectionColor ??
@@ -4,7 +4,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_ai_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/setting_ai_view/settings_ai_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_billing_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_manage_data_view.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_plan_view.dart';
@@ -13,6 +13,7 @@ DropdownMenuEntry<T> buildDropdownMenuEntry<T>(
Widget? leadingWidget,
Widget? trailingWidget,
String? fontFamily,
EdgeInsets padding = const EdgeInsets.symmetric(vertical: 4),
}) {
final fontFamilyUsed = fontFamily != null
? getGoogleFontSafely(fontFamily).fontFamily ?? defaultFontFamily
@@ -32,7 +33,7 @@ DropdownMenuEntry<T> buildDropdownMenuEntry<T>(
label: label,
leadingIcon: leadingWidget,
labelWidget: Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
padding: padding,
child: FlowyText.regular(
label,
fontSize: 14,
@@ -1,10 +1,14 @@
import 'package:flutter/material.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/util/color_to_hex_string.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_alert_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/utils/hex_opacity_string_extension.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flex_color_picker/flex_color_picker.dart';
import 'package:flowy_infra/size.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flowy_infra_ui/widget/dialog/styled_dialogs.dart';
@@ -90,24 +94,11 @@ class DocumentColorSettingDialogState
late TextEditingController hexController;
late TextEditingController opacityController;
void updateSelectedColor() {
if (widget.formKey.currentState!.validate()) {
setState(() {
final colorValue = int.tryParse(
hexController.text.combineHexWithOpacity(opacityController.text),
);
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
selectedColorOnDialog = Color(colorValue!);
widget.onChanged(selectedColorOnDialog!);
});
}
}
@override
void initState() {
super.initState();
selectedColorOnDialog = widget.currentColor;
currentColorHexString = widget.currentColor.toHexString();
currentColorHexString = ColorExtension(widget.currentColor).toHexString();
hexController = TextEditingController(
text: currentColorHexString.extractHex(),
);
@@ -145,17 +136,31 @@ class DocumentColorSettingDialogState
controller: hexController,
labelText: LocaleKeys.editor_hexValue.tr(),
hintText: '6fc9e7',
onChanged: (_) => updateSelectedColor(),
onFieldSubmitted: (_) => updateSelectedColor(),
onChanged: (_) => _updateSelectedColor(),
onFieldSubmitted: (_) => _updateSelectedColor(),
validator: (v) => validateHexValue(v, opacityController.text),
suffixIcon: Padding(
padding: const EdgeInsets.all(6.0),
child: FlowyIconButton(
onPressed: () => _showColorPickerDialog(
context: context,
currentColor: widget.currentColor,
updateColor: _updateColor,
),
icon: const FlowySvg(
FlowySvgs.m_aa_color_s,
size: Size.square(20),
),
),
),
),
const VSpace(8),
_ColorSettingTextField(
controller: opacityController,
labelText: LocaleKeys.editor_opacity.tr(),
hintText: '50',
onChanged: (_) => updateSelectedColor(),
onFieldSubmitted: (_) => updateSelectedColor(),
onChanged: (_) => _updateSelectedColor(),
onFieldSubmitted: (_) => _updateSelectedColor(),
validator: (value) => validateOpacityValue(value),
),
],
@@ -164,6 +169,28 @@ class DocumentColorSettingDialogState
],
);
}
void _updateSelectedColor() {
if (widget.formKey.currentState!.validate()) {
setState(() {
final colorValue = int.tryParse(
hexController.text.combineHexWithOpacity(opacityController.text),
);
// colorValue has been validated in the _ColorSettingTextField for hex value and it won't be null as this point
selectedColorOnDialog = Color(colorValue!);
widget.onChanged(selectedColorOnDialog!);
});
}
}
void _updateColor(Color color) {
setState(() {
hexController.text = ColorExtension(color).toHexString().extractHex();
opacityController.text =
ColorExtension(color).toHexString().extractOpacity();
});
_updateSelectedColor();
}
}
class _ColorSettingTextField extends StatelessWidget {
@@ -172,6 +199,7 @@ class _ColorSettingTextField extends StatelessWidget {
required this.labelText,
required this.hintText,
required this.onFieldSubmitted,
this.suffixIcon,
this.onChanged,
this.validator,
});
@@ -180,6 +208,7 @@ class _ColorSettingTextField extends StatelessWidget {
final String labelText;
final String hintText;
final void Function(String) onFieldSubmitted;
final Widget? suffixIcon;
final void Function(String)? onChanged;
final String? Function(String?)? validator;
@@ -191,6 +220,7 @@ class _ColorSettingTextField extends StatelessWidget {
decoration: InputDecoration(
labelText: labelText,
hintText: hintText,
suffixIcon: suffixIcon,
border: OutlineInputBorder(
borderSide: BorderSide(color: style.colorScheme.outline),
),
@@ -241,3 +271,159 @@ String? validateOpacityValue(String? value) {
}
return null;
}
const _kColorCircleWidth = 32.0;
const _kColorCircleHeight = 32.0;
const _kColorCircleRadius = 20.0;
const _kColorOpacityThumbRadius = 23.0;
const _kDialogButtonPaddingHorizontal = 24.0;
const _kDialogButtonPaddingVertical = 12.0;
const _kColorsColumnSpacing = 12.0;
class _ColorPicker extends StatelessWidget {
const _ColorPicker({
required this.selectedColor,
required this.onColorChanged,
});
final Color selectedColor;
final void Function(Color) onColorChanged;
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return ColorPicker(
width: _kColorCircleWidth,
height: _kColorCircleHeight,
borderRadius: _kColorCircleRadius,
enableOpacity: true,
opacityThumbRadius: _kColorOpacityThumbRadius,
columnSpacing: _kColorsColumnSpacing,
enableTooltips: false,
hasBorder: true,
borderColor: theme.colorScheme.outline,
pickersEnabled: const {
ColorPickerType.both: false,
ColorPickerType.primary: true,
ColorPickerType.accent: true,
ColorPickerType.wheel: true,
},
subheading: Text(
LocaleKeys.settings_appearance_documentSettings_colorShade.tr(),
style: theme.textTheme.labelLarge,
),
opacitySubheading: Text(
LocaleKeys.settings_appearance_documentSettings_opacity.tr(),
style: theme.textTheme.labelLarge,
),
onColorChanged: onColorChanged,
);
}
}
class _ColorPickerActions extends StatelessWidget {
const _ColorPickerActions({
required this.onReset,
required this.onUpdate,
});
final VoidCallback onReset;
final VoidCallback onUpdate;
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 24,
child: FlowyTextButton(
LocaleKeys.button_cancel.tr(),
padding: const EdgeInsets.symmetric(
horizontal: _kDialogButtonPaddingHorizontal,
vertical: _kDialogButtonPaddingVertical,
),
fontColor: AFThemeExtension.of(context).textColor,
fillColor: Colors.transparent,
hoverColor: Colors.transparent,
radius: Corners.s12Border,
onPressed: onReset,
),
),
const HSpace(8),
SizedBox(
height: 48,
child: FlowyTextButton(
LocaleKeys.button_done.tr(),
padding: const EdgeInsets.symmetric(
horizontal: _kDialogButtonPaddingHorizontal,
vertical: _kDialogButtonPaddingVertical,
),
radius: Corners.s12Border,
fontHoverColor: Colors.white,
fillColor: Theme.of(context).colorScheme.primary,
hoverColor: const Color(0xFF005483),
onPressed: onUpdate,
),
),
],
);
}
}
void _showColorPickerDialog({
required BuildContext context,
String? title,
required Color currentColor,
required void Function(Color) updateColor,
}) {
Color selectedColor = currentColor;
showDialog(
context: context,
barrierColor: const Color.fromARGB(128, 0, 0, 0),
builder: (context) => FlowyDialog(
expandHeight: false,
title: Row(
children: [
const FlowySvg(FlowySvgs.m_aa_color_s),
const HSpace(12),
FlowyText(
title ??
LocaleKeys.settings_appearance_documentSettings_pickColor.tr(),
fontSize: 20,
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_ColorPicker(
selectedColor: selectedColor,
onColorChanged: (color) => selectedColor = color,
),
Padding(
padding: const EdgeInsets.all(16),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
const HSpace(8),
_ColorPickerActions(
onReset: () {
updateColor(currentColor);
Navigator.of(context).pop();
},
onUpdate: () {
updateColor(selectedColor);
Navigator.of(context).pop();
},
),
],
),
),
],
),
),
);
}
@@ -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,
),
),
);
},
);
}
@@ -1,9 +1,12 @@
import 'package:appflowy/core/helpers/url_launcher.dart';
import 'package:appflowy/env/env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/document/application/document_bloc.dart';
import 'package:appflowy/startup/tasks/rust_sdk.dart';
import 'package:appflowy/util/theme_extension.dart';
import 'package:appflowy/workspace/presentation/home/toast.dart';
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy_popover/appflowy_popover.dart';
import 'package:device_info_plus/device_info_plus.dart';
@@ -203,9 +206,24 @@ class FlowyVersionDescription extends CustomActionCell {
thickness: 1.0,
),
const VSpace(6),
FlowyText(
"$appName $version",
color: Theme.of(context).hintColor,
GestureDetector(
behavior: HitTestBehavior.opaque,
onDoubleTap: () {
if (Env.internalBuild != '1') {
return;
}
enableDocumentInternalLog = !enableDocumentInternalLog;
showToastNotification(
context,
message: enableDocumentInternalLog
? 'Enabled Internal Log'
: 'Disabled Internal Log',
);
},
child: FlowyText(
'$appName $version',
color: Theme.of(context).hintColor,
),
),
],
).padding(
@@ -1,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,
+119 -7
View File
@@ -53,11 +53,11 @@ packages:
dependency: "direct main"
description:
path: "."
ref: e8ee051
resolved-ref: e8ee051719eded6621ccdc2722f696411c020209
ref: e0d673a
resolved-ref: e0d673afbbbcaf9df0276f7e0b6405d8f6e98112
url: "https://github.com/AppFlowy-IO/appflowy-editor.git"
source: git
version: "3.0.0"
version: "3.1.0"
appflowy_editor_plugins:
dependency: "direct main"
description:
@@ -121,6 +121,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
barcode:
dependency: transitive
description:
name: barcode
sha256: ab180ce22c6555d77d45f0178a523669db67f95856e3378259ef2ffeb43e6003
url: "https://pub.dev"
source: hosted
version: "2.2.8"
bidi:
dependency: transitive
description:
name: bidi
sha256: "1a7d0c696324b2089f72e7671fd1f1f64fef44c980f3cebc84e803967c597b63"
url: "https://pub.dev"
source: hosted
version: "2.0.10"
bitsdojo_window:
dependency: "direct main"
description:
@@ -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:
+8 -2
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 0.6.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:
File diff suppressed because it is too large. Load diff
+26 -9
View File
@@ -29,7 +29,8 @@ tokio = "1.34.0"
tokio-stream = "0.1.14"
async-trait = "0.1.74"
chrono = { version = "0.4.31", default-features = false, features = ["clock"] }
yrs = "0.18.8"
zip = "2.1.3"
yrs = "0.19.1"
# Please use the following script to update collab.
# Working directory: frontend
#
@@ -52,7 +53,7 @@ collab-user = { version = "0.2" }
# Run the script:
# scripts/tool/update_client_api_rev.sh new_rev_id
# ⚠️⚠️⚠️️
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "9884d93aa2805013f36a79c1757174a0b5063065" }
client-api = { git = "https://github.com/AppFlowy-IO/AppFlowy-Cloud", rev = "eebdbcad79a35b07305affdd36f16d9ce95c5a18" }
[dependencies]
serde_json.workspace = true
@@ -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