Compare commits
36
Commits
build/test
..
0.6.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6d0c9f766b | ||
|
|
7a3f013a87 | ||
|
|
8c1520b273 | ||
|
|
a7b850e752 | ||
|
|
42dc6017c6 | ||
|
|
f9fba6b561 | ||
|
|
46896b5a2b | ||
|
|
2b8dca209e | ||
|
|
c78f23e1c0 | ||
|
|
50f5be3e75 | ||
|
|
e1c68c1b72 | ||
|
|
3bcadff152 | ||
|
|
b09ff040f1 | ||
|
|
bf087bba5d | ||
|
|
605a53f2ae | ||
|
|
ecc5555a4e | ||
|
|
d2a5bab6d9 | ||
|
|
a1dec0f269 | ||
|
|
b24fbc6b60 | ||
|
|
bbb3f95a13 | ||
|
|
7859fc7922 | ||
|
|
f812040f04 | ||
|
|
7d96c2dfd4 | ||
|
|
e4132ed217 | ||
|
|
a7f40b2adc | ||
|
|
c996c9c28e | ||
|
|
f59b8a2e5c | ||
|
|
f7480cba16 | ||
|
|
271b250eac | ||
|
|
6b1e7b6ac8 | ||
|
|
bf2a00b133 | ||
|
|
a8ed93054c | ||
|
|
b9ad2768cf | ||
|
|
54c9d12171 | ||
|
|
40312f4260 | ||
|
|
d2ca41c8f5 |
No files matched your search
@@ -69,4 +69,4 @@ jobs:
|
||||
SCRIPT_AFTER: |
|
||||
docker build -t appflowy-web-app .
|
||||
docker rm -f appflowy-web-app || true
|
||||
docker run -d -p 80:80 -p 443:443 --env-file .env --name appflowy-web-app appflowy-web-app
|
||||
docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app
|
||||
@@ -1,83 +0,0 @@
|
||||
name: WEB-CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build:
|
||||
description: 'Build the web app'
|
||||
required: true
|
||||
default: 'true'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
CARGO_MAKE_VERSION: "0.36.6"
|
||||
|
||||
jobs:
|
||||
web-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ ubuntu-latest ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Cache Rust Dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: rust-dependencies-${{ runner.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_web/appflowy_wasm
|
||||
|
||||
# TODO: Can combine caching deps and node_modules in one
|
||||
# See Glob patterns: https://github.com/actions/toolkit/tree/main/packages/glob
|
||||
- name: Cache Node.js dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ runner.os }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: frontend/appflowy_web/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: cargo install wasm-pack
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
|
||||
|
||||
- name: install dependencies
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
npm install -g pnpm@${{ env.PNPM_VERSION }}
|
||||
cargo make install_web_protobuf
|
||||
|
||||
- name: Build
|
||||
working-directory: frontend/appflowy_web
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build_release_wasm
|
||||
@@ -1,4 +1,21 @@
|
||||
# Release Notes
|
||||
## Version 0.6.2 - 01/07/2024
|
||||
### New Features
|
||||
- Added support for duplicating spaces.
|
||||
- Added support for moving pages across spaces.
|
||||
- Undo markdown formatting with `Ctrl + Z` or `Cmd + Z`.
|
||||
- Improved shortcuts settings UI.
|
||||
### Bug Fixes
|
||||
- Fixed unable to zoom in with `Ctrl` and `+` or `Cmd` and `+` on some keyboards.
|
||||
- Fixed unable to paste nested lists in existing lists.
|
||||
|
||||
## Version 0.6.1 - 22/06/2024
|
||||
### New Features
|
||||
- Introduced the "Space" feature to help you organize your pages more efficiently.
|
||||
### Bug Fixes
|
||||
- Resolved shortcut conflicts on the board page.
|
||||
- Resolved an issue where underscores could cause the editor to freeze.
|
||||
|
||||
## Version 0.6.0 - 19/06/2024
|
||||
### New Features
|
||||
- Introduced the "Space" feature to help you organize your pages more efficiently.
|
||||
|
||||
@@ -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.1"
|
||||
APPFLOWY_VERSION = "0.6.2"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
@@ -50,7 +50,6 @@ APP_ENVIRONMENT = "local"
|
||||
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
|
||||
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
|
||||
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
|
||||
WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm"
|
||||
TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend"
|
||||
# Test default config
|
||||
TEST_CRATE_TYPE = "cdylib"
|
||||
|
||||
@@ -46,7 +46,6 @@ void main() {
|
||||
await tester.ime.insertText(inputContent);
|
||||
expect(find.text(inputContent, findRichText: true), findsOneWidget);
|
||||
|
||||
// TODO(nathan): remove the await
|
||||
// 6 seconds for data sync
|
||||
await tester.waitForSeconds(6);
|
||||
|
||||
|
||||
+68
-27
@@ -1,10 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
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/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -116,6 +117,53 @@ void main() {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste bulleted list in numbered list', (tester) async {
|
||||
const inAppJson =
|
||||
'{"document":{"type":"page","children":[{"type":"bulleted_list","children":[{"type":"bulleted_list","data":{"delta":[{"insert":"World"}]}}],"data":{"delta":[{"insert":"Hello"}]}}]}}';
|
||||
|
||||
await tester.pasteContent(
|
||||
inAppJson: inAppJson,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
// Insert two numbered list nodes
|
||||
// 1. Parent One
|
||||
// 2.
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: NumberedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "One"},
|
||||
],
|
||||
},
|
||||
),
|
||||
Node(
|
||||
type: NumberedListBlockKeys.type,
|
||||
attributes: {'delta': []},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection.collapsed(Position(path: [1]));
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
(editorState) {
|
||||
final secondNode = editorState.getNodeAtPath([1]);
|
||||
expect(secondNode?.delta?.toPlainText(), 'Hello');
|
||||
expect(secondNode?.children.length, 1);
|
||||
|
||||
final childNode = secondNode?.children.first;
|
||||
expect(childNode?.delta?.toPlainText(), 'World');
|
||||
expect(childNode?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
@@ -210,42 +258,33 @@ void main() {
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
},
|
||||
);
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'auto convert url to link preview block',
|
||||
(widgetTester) async {
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await widgetTester.pasteContent(
|
||||
plainText: url,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
},
|
||||
);
|
||||
await tester.pasteContent(plainText: url, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -256,6 +295,7 @@ extension on WidgetTester {
|
||||
Future<void> Function(EditorState editorState)? beforeTest,
|
||||
String? plainText,
|
||||
String? html,
|
||||
String? inAppJson,
|
||||
(String, Uint8List?)? image,
|
||||
}) async {
|
||||
await initializeAppFlowy();
|
||||
@@ -271,6 +311,7 @@ extension on WidgetTester {
|
||||
ClipboardServiceData(
|
||||
plainText: plainText,
|
||||
html: html,
|
||||
inAppJson: inAppJson,
|
||||
image: image,
|
||||
),
|
||||
);
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
||||
@@ -103,8 +103,8 @@ Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
|
||||
}
|
||||
|
||||
Future<void> mockOpenAIRepository() async {
|
||||
await getIt.unregister<OpenAIRepository>();
|
||||
getIt.registerFactoryAsync<OpenAIRepository>(
|
||||
await getIt.unregister<AIRepository>();
|
||||
getIt.registerFactoryAsync<AIRepository>(
|
||||
() => Future.value(
|
||||
MockOpenAIRepository(),
|
||||
),
|
||||
|
||||
@@ -44,7 +44,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(TextCompletionResponse response) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(OpenAIError error) onError,
|
||||
required void Function(AIError error) onError,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
|
||||
@@ -170,7 +170,7 @@ SPEC CHECKSUMS:
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
@@ -191,4 +191,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.11.3
|
||||
@@ -18,6 +18,13 @@ class KVKeys {
|
||||
/// {'dx': 10.0, 'dy': 10.0}
|
||||
static const String windowPosition = 'windowPosition';
|
||||
|
||||
/// The key for saving the window status
|
||||
///
|
||||
/// The value is a json string with the following format:
|
||||
/// { 'windowMaximized': true }
|
||||
///
|
||||
static const String windowMaximized = 'windowMaximized';
|
||||
|
||||
static const String kDocumentAppearanceFontSize =
|
||||
'kDocumentAppearanceFontSize';
|
||||
static const String kDocumentAppearanceFontFamily =
|
||||
|
||||
@@ -54,6 +54,7 @@ class _MobileSpaceState extends State<MobileSpace> {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
index: 0,
|
||||
),
|
||||
);
|
||||
@@ -99,6 +100,7 @@ class _MobileSpaceState extends State<MobileSpace> {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/chat_page.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
@@ -7,7 +9,6 @@ import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class AIChatPluginBuilder extends PluginBuilder {
|
||||
@@ -65,6 +66,12 @@ class AIChatPagePlugin extends Plugin {
|
||||
_viewInfoBloc = ViewInfoBloc(view: notifier.view)
|
||||
..add(const ViewInfoEvent.started());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_viewInfoBloc.close();
|
||||
notifier.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
@@ -111,4 +118,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
|
||||
@override
|
||||
EdgeInsets get contentPadding => EdgeInsets.zero;
|
||||
}
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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/ai_message_bubble.dart';
|
||||
@@ -10,12 +13,10 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.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/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
|
||||
|
||||
import 'presentation/chat_input.dart';
|
||||
import 'presentation/chat_popmenu.dart';
|
||||
@@ -27,11 +28,11 @@ import 'presentation/message/user_text_message.dart';
|
||||
|
||||
class AIChatUILayout {
|
||||
static EdgeInsets get chatPadding =>
|
||||
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 70);
|
||||
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 20);
|
||||
|
||||
static EdgeInsets get welcomePagePadding => isMobile
|
||||
? const EdgeInsets.symmetric(horizontal: 20)
|
||||
: const EdgeInsets.symmetric(horizontal: 100);
|
||||
: const EdgeInsets.symmetric(horizontal: 50);
|
||||
|
||||
static double get messageWidthRatio => 0.85;
|
||||
|
||||
@@ -44,7 +45,8 @@ class AIChatUILayout {
|
||||
query.padding.right,
|
||||
query.viewInsets.bottom + query.padding.bottom,
|
||||
)
|
||||
: const EdgeInsets.symmetric(horizontal: 70);
|
||||
: const EdgeInsets.symmetric(horizontal: 50) +
|
||||
const EdgeInsets.only(bottom: 20);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,92 +79,89 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||
return buildChatWidget();
|
||||
} else {
|
||||
return Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
||||
fontSize: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
||||
fontSize: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildChatWidget() {
|
||||
return SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: AIChatUILayout.chatPadding,
|
||||
child: BlocProvider(
|
||||
create: (context) => ChatBloc(
|
||||
view: widget.view,
|
||||
userProfile: widget.userProfile,
|
||||
)..add(const ChatEvent.initialLoad()),
|
||||
child: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (blocContext, state) {
|
||||
return Chat(
|
||||
messages: state.messages,
|
||||
onAttachmentPressed: () {},
|
||||
onSendPressed: (types.PartialText message) {
|
||||
// We use custom bottom widget for chat input, so
|
||||
// do not need to handle this event.
|
||||
},
|
||||
customBottomWidget: buildChatInput(blocContext),
|
||||
user: _user,
|
||||
theme: buildTheme(context),
|
||||
onEndReached: () async {
|
||||
if (state.hasMorePrevMessage &&
|
||||
state.loadingPreviousStatus !=
|
||||
const LoadingState.loading()) {
|
||||
blocContext
|
||||
.read<ChatBloc>()
|
||||
.add(const ChatEvent.startLoadingPrevMessage());
|
||||
}
|
||||
},
|
||||
emptyState: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (context, state) {
|
||||
return state.initialLoadingStatus ==
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 784),
|
||||
child: BlocProvider(
|
||||
create: (_) => ChatBloc(
|
||||
view: widget.view,
|
||||
userProfile: widget.userProfile,
|
||||
)..add(const ChatEvent.initialLoad()),
|
||||
child: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (blocContext, state) => Chat(
|
||||
messages: state.messages,
|
||||
onSendPressed: (_) {
|
||||
// We use custom bottom widget for chat input, so
|
||||
// do not need to handle this event.
|
||||
},
|
||||
customBottomWidget: buildChatInput(blocContext),
|
||||
user: _user,
|
||||
theme: buildTheme(context),
|
||||
onEndReached: () async {
|
||||
if (state.hasMorePrevMessage &&
|
||||
state.loadingPreviousStatus !=
|
||||
const LoadingState.loading()) {
|
||||
blocContext
|
||||
.read<ChatBloc>()
|
||||
.add(const ChatEvent.startLoadingPrevMessage());
|
||||
}
|
||||
},
|
||||
emptyState: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (_, state) => state.initialLoadingStatus ==
|
||||
const LoadingState.finish()
|
||||
? Padding(
|
||||
padding: AIChatUILayout.welcomePagePadding,
|
||||
child: ChatWelcomePage(
|
||||
onSelectedQuestion: (question) {
|
||||
blocContext
|
||||
.read<ChatBloc>()
|
||||
.add(ChatEvent.sendMessage(question));
|
||||
},
|
||||
onSelectedQuestion: (question) => blocContext
|
||||
.read<ChatBloc>()
|
||||
.add(ChatEvent.sendMessage(question)),
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
),
|
||||
),
|
||||
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
||||
textMessageBuilder: (
|
||||
textMessage, {
|
||||
required messageWidth,
|
||||
required showName,
|
||||
}) =>
|
||||
_buildAITextMessage(blocContext, textMessage),
|
||||
bubbleBuilder: (
|
||||
child, {
|
||||
required message,
|
||||
required nextMessageInGroup,
|
||||
}) {
|
||||
if (message.author.id == _user.id) {
|
||||
return ChatUserMessageBubble(
|
||||
message: message,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
return _buildAIBubble(message, blocContext, state, child);
|
||||
},
|
||||
),
|
||||
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
||||
textMessageBuilder: (
|
||||
textMessage, {
|
||||
required messageWidth,
|
||||
required showName,
|
||||
}) {
|
||||
return _buildAITextMessage(blocContext, textMessage);
|
||||
},
|
||||
bubbleBuilder: (
|
||||
child, {
|
||||
required message,
|
||||
required nextMessageInGroup,
|
||||
}) {
|
||||
if (message.author.id == _user.id) {
|
||||
return ChatUserMessageBubble(
|
||||
message: message,
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return _buildAIBubble(message, blocContext, state, child);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
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';
|
||||
|
||||
@@ -31,19 +32,15 @@ class ChatWelcomePage extends StatelessWidget {
|
||||
size: Size.square(44),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: isMobile ? 2 : 4,
|
||||
crossAxisSpacing: 6,
|
||||
mainAxisSpacing: 6,
|
||||
childAspectRatio: 16.0 / 9.0,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => WelcomeQuestion(
|
||||
question: items[index],
|
||||
onSelected: onSelectedQuestion,
|
||||
),
|
||||
Wrap(
|
||||
children: items
|
||||
.map(
|
||||
(i) => WelcomeQuestion(
|
||||
question: i,
|
||||
onSelected: onSelectedQuestion,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
|
||||
// use a global value to store the selected emoji to prevent reloading every time.
|
||||
@@ -37,9 +38,7 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
||||
EmojiData.builtIn().then(
|
||||
(value) {
|
||||
kCachedEmojiData = value;
|
||||
setState(() {
|
||||
emojiData = value;
|
||||
});
|
||||
setState(() => emojiData = value);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/view/view_cache.dart';
|
||||
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
|
||||
@@ -12,7 +14,6 @@ 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:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'defines.dart';
|
||||
import 'row/row_cache.dart';
|
||||
@@ -223,6 +224,7 @@ class DatabaseController {
|
||||
_databaseCallbacks.clear();
|
||||
_groupCallbacks.clear();
|
||||
_layoutCallbacks.clear();
|
||||
_isLoading.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadGroups() async {
|
||||
|
||||
+5
-10
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@@ -19,12 +21,12 @@ 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/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../application/row/row_controller.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
|
||||
import 'calendar_day.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
@@ -90,12 +92,11 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_calendarState = GlobalKey<MonthViewState>();
|
||||
_calendarBloc = CalendarBloc(
|
||||
databaseController: widget.databaseController,
|
||||
)..add(const CalendarEvent.initial());
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -378,13 +379,7 @@ class UnscheduledEventsButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
|
||||
late final PopoverController _popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_popoverController = PopoverController();
|
||||
}
|
||||
final PopoverController _popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
+6
@@ -30,6 +30,12 @@ class CalendarLayoutSetting extends StatefulWidget {
|
||||
class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
|
||||
final PopoverMutex popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
|
||||
+11
-4
@@ -1,18 +1,19 @@
|
||||
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/database/grid/application/filter/checkbox_filter_editor_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class CheckboxFilterChoicechip extends StatefulWidget {
|
||||
@@ -30,9 +31,9 @@ class _CheckboxFilterChoicechipState extends State<CheckboxFilterChoicechip> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
bloc = CheckboxFilterEditorBloc(filterInfo: widget.filterInfo)
|
||||
..add(const CheckboxFilterEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -82,6 +83,12 @@ class CheckboxFilterEditor extends StatefulWidget {
|
||||
class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
|
||||
+8
-6
@@ -1,12 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/checklist_filter_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../condition_button.dart';
|
||||
import '../../disclosure_button.dart';
|
||||
import '../../filter_info.dart';
|
||||
@@ -23,20 +25,20 @@ class ChecklistFilterChoicechip extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ChecklistFilterChoicechipState extends State<ChecklistFilterChoicechip> {
|
||||
late ChecklistFilterEditorBloc bloc;
|
||||
late PopoverMutex popoverMutex;
|
||||
late final ChecklistFilterEditorBloc bloc;
|
||||
final PopoverMutex popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverMutex = PopoverMutex();
|
||||
super.initState();
|
||||
bloc = ChecklistFilterEditorBloc(filterInfo: widget.filterInfo);
|
||||
bloc.add(const ChecklistFilterEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -1,3 +1,5 @@
|
||||
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/database/grid/application/filter/number_filter_editor_bloc.dart';
|
||||
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class NumberFilterChoiceChip extends StatefulWidget {
|
||||
@@ -64,6 +66,12 @@ class NumberFilterEditor extends StatefulWidget {
|
||||
class _NumberFilterEditorState extends State<NumberFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<NumberFilterEditorBloc, NumberFilterEditorState>(
|
||||
|
||||
+10
-2
@@ -1,14 +1,16 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../disclosure_button.dart';
|
||||
import '../../filter_info.dart';
|
||||
import '../choicechip.dart';
|
||||
|
||||
import 'condition_list.dart';
|
||||
import 'option_list.dart';
|
||||
import 'select_option_loader.dart';
|
||||
@@ -29,6 +31,7 @@ class _SelectOptionFilterChoicechipState
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
|
||||
bloc = SelectOptionFilterEditorBloc(
|
||||
filterInfo: widget.filterInfo,
|
||||
@@ -43,7 +46,6 @@ class _SelectOptionFilterChoicechipState
|
||||
);
|
||||
}
|
||||
bloc.add(const SelectOptionFilterEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -90,6 +92,12 @@ class SelectOptionFilterEditor extends StatefulWidget {
|
||||
class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
|
||||
+9
-1
@@ -1,3 +1,5 @@
|
||||
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/database/grid/application/filter/text_filter_editor_bloc.dart';
|
||||
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class TextFilterChoicechip extends StatelessWidget {
|
||||
@@ -72,6 +74,12 @@ class TextFilterEditor extends StatefulWidget {
|
||||
class _TextFilterEditorState extends State<TextFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
||||
|
||||
+9
-1
@@ -1,3 +1,5 @@
|
||||
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/database/grid/application/filter/time_filter_editor_bloc.dart';
|
||||
@@ -6,12 +8,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class TimeFilterChoiceChip extends StatefulWidget {
|
||||
@@ -64,6 +66,12 @@ class TimeFilterEditor extends StatefulWidget {
|
||||
class _TimeFilterEditorState extends State<TimeFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
|
||||
|
||||
+4
-3
@@ -1,3 +1,5 @@
|
||||
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/database/application/field/field_info.dart';
|
||||
@@ -10,7 +12,6 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../application/field/field_controller.dart';
|
||||
@@ -35,15 +36,15 @@ class GridCreateFilterList extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GridCreateFilterListState extends State<GridCreateFilterList> {
|
||||
late GridCreateFilterBloc editBloc;
|
||||
late final GridCreateFilterBloc editBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
editBloc = GridCreateFilterBloc(
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
)..add(const GridCreateFilterEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+3
-9
@@ -1,3 +1,5 @@
|
||||
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/database/application/field/field_controller.dart';
|
||||
@@ -5,9 +7,7 @@ import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bl
|
||||
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/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'create_filter_list.dart';
|
||||
@@ -77,13 +77,7 @@ class AddFilterButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AddFilterButtonState extends State<AddFilterButton> {
|
||||
late PopoverController popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
final PopoverController popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
+3
-3
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
@@ -9,7 +11,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../layout/sizes.dart';
|
||||
@@ -41,13 +42,12 @@ class GridFieldCell extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GridFieldCellState extends State<GridFieldCell> {
|
||||
final PopoverController popoverController = PopoverController();
|
||||
late final FieldCellBloc _bloc;
|
||||
late PopoverController popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
popoverController = PopoverController();
|
||||
_bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);
|
||||
if (widget.isEditing) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
|
||||
+8
-2
@@ -1,5 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';
|
||||
@@ -7,10 +9,8 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.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/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'create_sort_list.dart';
|
||||
@@ -28,6 +28,12 @@ class SortEditor extends StatefulWidget {
|
||||
class _SortEditorState extends State<SortEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SortEditorBloc, SortEditorState>(
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
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/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -56,7 +56,7 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 1,
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
),
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -67,7 +67,7 @@ class DesktopRowDetailSelectOptionCellSkin
|
||||
return SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 1,
|
||||
vertical: 4,
|
||||
horizontal: 8,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -96,7 +96,6 @@ class SelectOptionTag extends StatelessWidget {
|
||||
);
|
||||
|
||||
return Container(
|
||||
height: 20,
|
||||
padding: onRemove == null ? padding : padding.copyWith(right: 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: optionColor,
|
||||
|
||||
+3
-9
@@ -314,13 +314,7 @@ class _SelectOptionCell extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SelectOptionCellState extends State<_SelectOptionCell> {
|
||||
late PopoverController _popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
_popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
final _popoverController = PopoverController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -480,11 +474,11 @@ class SelectOptionTagCell extends StatelessWidget {
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 6.0,
|
||||
vertical: 4.0,
|
||||
),
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+4
-1
@@ -132,7 +132,10 @@ class _SelectOptionTextFieldState extends State<SelectOptionTextField> {
|
||||
(option) => SelectOptionTag(
|
||||
option: option,
|
||||
onRemove: (option) => widget.onRemove(option),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 1),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList();
|
||||
|
||||
@@ -370,13 +370,7 @@ class FieldDetailsEditor extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _FieldDetailsEditorState extends State<FieldDetailsEditor> {
|
||||
late PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverMutex = PopoverMutex();
|
||||
super.initState();
|
||||
}
|
||||
final PopoverMutex popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@@ -575,10 +569,7 @@ class _FieldNameTextFieldState extends State<FieldNameTextField> {
|
||||
}
|
||||
|
||||
class SwitchFieldButton extends StatefulWidget {
|
||||
const SwitchFieldButton({
|
||||
super.key,
|
||||
required this.popoverMutex,
|
||||
});
|
||||
const SwitchFieldButton({super.key, required this.popoverMutex});
|
||||
|
||||
final PopoverMutex popoverMutex;
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
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/database/application/cell/cell_controller.dart';
|
||||
@@ -18,13 +21,12 @@ import 'package:collection/collection.dart';
|
||||
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:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'accessory/cell_accessory.dart';
|
||||
import '../cell/editable_cell_builder.dart';
|
||||
|
||||
import 'accessory/cell_accessory.dart';
|
||||
|
||||
/// Display the row properties in a list. Only used in [RowDetailPage].
|
||||
class RowPropertyList extends StatelessWidget {
|
||||
const RowPropertyList({
|
||||
@@ -363,15 +365,9 @@ class CreateRowFieldButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _CreateRowFieldButtonState extends State<CreateRowFieldButton> {
|
||||
late PopoverController popoverController;
|
||||
final PopoverController popoverController = PopoverController();
|
||||
FieldPB? createdField;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AppFlowyPopover(
|
||||
|
||||
+9
-2
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/setting/database_setting_action.dart';
|
||||
@@ -6,7 +8,6 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class DatabaseSettingsList extends StatefulWidget {
|
||||
const DatabaseSettingsList({
|
||||
@@ -21,7 +22,13 @@ class DatabaseSettingsList extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _DatabaseSettingsListState extends State<DatabaseSettingsList> {
|
||||
late final PopoverMutex popoverMutex = PopoverMutex();
|
||||
final PopoverMutex popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
+8
-1
@@ -1,5 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
@@ -13,7 +15,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DatabasePropertyList extends StatefulWidget {
|
||||
@@ -43,6 +44,12 @@ class _DatabasePropertyListState extends State<DatabasePropertyList> {
|
||||
)..add(const DatabasePropertyEvent.initial());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider<DatabasePropertyBloc>.value(
|
||||
|
||||
@@ -8,7 +8,6 @@ import 'package:appflowy/plugins/document/application/document_data_pb_extension
|
||||
import 'package:appflowy/plugins/document/application/document_listener.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_service.dart';
|
||||
import 'package:appflowy/plugins/document/application/editor_transaction_adapter.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/migration/editor_migration.dart';
|
||||
import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
@@ -19,7 +18,6 @@ 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/workspace/application/view/view_service.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';
|
||||
@@ -118,11 +116,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
final result = await _fetchDocumentState();
|
||||
_onViewChanged();
|
||||
_onDocumentChanged();
|
||||
result.onSuccess((s) {
|
||||
if (s != null) {
|
||||
_migrateCover(s);
|
||||
}
|
||||
});
|
||||
final newState = await result.fold(
|
||||
(s) async {
|
||||
final userProfilePB =
|
||||
@@ -390,14 +383,6 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
metadata: jsonEncode(metadata.toJson()),
|
||||
);
|
||||
}
|
||||
|
||||
// from version 0.5.5, the cover is stored in the view.ext
|
||||
Future<void> _migrateCover(EditorState editorState) async {
|
||||
final view = await ViewBackendService.getView(documentId);
|
||||
view.onSuccess((s) {
|
||||
return EditorMigration.migrateCoverIfNeeded(s, editorState);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
|
||||
+29
-117
@@ -1,102 +1,52 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.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_result/appflowy_result.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'document_share_bloc.freezed.dart';
|
||||
|
||||
const _url = 'https://test.appflowy.io';
|
||||
|
||||
class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
|
||||
DocumentShareBloc({
|
||||
required this.view,
|
||||
}) : super(DocumentShareState.initial()) {
|
||||
}) : super(const DocumentShareState.initial()) {
|
||||
on<DocumentShareEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final publishInfo = await ViewBackendService.getPublishInfo(view);
|
||||
publishInfo.fold((s) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: true,
|
||||
url: '$_url/${s.namespace}/${s.publishName}',
|
||||
),
|
||||
);
|
||||
}, (f) {
|
||||
emit(state.copyWith(isPublished: false, url: ''));
|
||||
});
|
||||
},
|
||||
share: (type, path) async {
|
||||
if (DocumentShareType.unimplemented.contains(type)) {
|
||||
Log.error('DocumentShareType $type is not implemented');
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(isLoading: true));
|
||||
emit(const DocumentShareState.loading());
|
||||
|
||||
final result = await _export(type, path);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
exportResult: result,
|
||||
),
|
||||
);
|
||||
},
|
||||
publish: (url) async {
|
||||
// todo: optimize the logic
|
||||
const prefix = 'appflowy';
|
||||
final name = '${view.name}-${uuid()}'.substring(0, 19);
|
||||
|
||||
// set space name
|
||||
|
||||
try {
|
||||
final nameSpace =
|
||||
await ViewBackendService.getPublishNameSpace().getOrThrow();
|
||||
if (nameSpace.namespace != prefix) {
|
||||
await ViewBackendService.setPublishNameSpace(prefix).getOrThrow();
|
||||
}
|
||||
|
||||
final result = await ViewBackendService.publish(view, name: name);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: true,
|
||||
publishResult: result,
|
||||
url: '$_url/${nameSpace.namespace}/$name',
|
||||
),
|
||||
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),
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error('publish error: $e');
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: false,
|
||||
publishResult: FlowyResult.failure(
|
||||
FlowyError(msg: 'publish error: $e'),
|
||||
),
|
||||
url: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
unPublish: () async {
|
||||
await ViewBackendService.unpublish(view);
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: false,
|
||||
publishResult: null,
|
||||
url: '',
|
||||
),
|
||||
);
|
||||
emit(DocumentShareState.finish(result));
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -104,31 +54,6 @@ class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
late final exporter = DocumentExporter(view);
|
||||
|
||||
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);
|
||||
return ExportDataPB()
|
||||
@@ -168,28 +93,15 @@ enum DocumentShareType {
|
||||
|
||||
@freezed
|
||||
class DocumentShareEvent with _$DocumentShareEvent {
|
||||
const factory DocumentShareEvent.initial() = _Initial;
|
||||
const factory DocumentShareEvent.share(
|
||||
DocumentShareType type,
|
||||
String? path,
|
||||
) = _Share;
|
||||
const factory DocumentShareEvent.publish(String url) = _Publish;
|
||||
const factory DocumentShareEvent.unPublish() = _UnPublish;
|
||||
const factory DocumentShareEvent.share(DocumentShareType type, String? path) =
|
||||
Share;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentShareState with _$DocumentShareState {
|
||||
const factory DocumentShareState({
|
||||
required bool isLoading,
|
||||
FlowyResult<ExportDataPB, FlowyError>? exportResult,
|
||||
required bool isPublished,
|
||||
FlowyResult<void, FlowyError>? publishResult,
|
||||
required String url,
|
||||
}) = _DocumentShareState;
|
||||
|
||||
factory DocumentShareState.initial() => const DocumentShareState(
|
||||
isLoading: false,
|
||||
isPublished: false,
|
||||
url: '',
|
||||
);
|
||||
const factory DocumentShareState.initial() = _Initial;
|
||||
const factory DocumentShareState.loading() = _Loading;
|
||||
const factory DocumentShareState.finish(
|
||||
FlowyResult<ExportDataPB, FlowyError> successOrFail,
|
||||
) = _Finish;
|
||||
}
|
||||
+2
-3
@@ -1,6 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
@@ -13,6 +10,8 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart' hide TextDirection;
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
Map<String, BlockComponentBuilder> getEditorBuilderMap({
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import 'dart:ui' as ui;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||
@@ -31,6 +28,8 @@ import 'package:collection/collection.dart';
|
||||
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:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
final codeBlockLocalization = CodeBlockLocalizations(
|
||||
@@ -215,7 +214,11 @@ class _AppFlowyEditorPageState extends State<AppFlowyEditorPage> {
|
||||
|
||||
AFFocusManager? focusManager;
|
||||
|
||||
void _loseFocus() => widget.editorState.selection = null;
|
||||
void _loseFocus() {
|
||||
if (!widget.editorState.isDisposed) {
|
||||
widget.editorState.selection = null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
|
||||
+2
-1
@@ -196,6 +196,7 @@ class _CalloutBlockComponentWidgetState
|
||||
), // force to refresh the popover state
|
||||
title: '',
|
||||
emoji: emoji,
|
||||
emojiSize: 16.0,
|
||||
onSubmitted: (emoji, controller) {
|
||||
setEmoji(emoji);
|
||||
controller?.close();
|
||||
@@ -204,7 +205,7 @@ class _CalloutBlockComponentWidgetState
|
||||
),
|
||||
Flexible(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: const EdgeInsets.symmetric(vertical: 6.0),
|
||||
child: buildCalloutBlockComponent(context, textDirection),
|
||||
),
|
||||
),
|
||||
|
||||
+2
-1
@@ -1,9 +1,10 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Copy.
|
||||
///
|
||||
|
||||
+18
-10
@@ -1,3 +1,6 @@
|
||||
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';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/paste_from_html.dart';
|
||||
@@ -7,11 +10,9 @@ 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';
|
||||
|
||||
/// Paste.
|
||||
///
|
||||
/// - support
|
||||
/// - desktop
|
||||
/// - web
|
||||
@@ -41,8 +42,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
|
||||
final image = data.image;
|
||||
|
||||
// paste as link preview
|
||||
final result = await _pasteAsLinkPreview(editorState, plainText);
|
||||
if (result) {
|
||||
if (await _pasteAsLinkPreview(editorState, plainText)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -55,23 +55,31 @@ 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) {
|
||||
await editorState.deleteSelectionIfNeeded();
|
||||
final result = await editorState.pasteInAppJson(inAppJson);
|
||||
if (result) {
|
||||
if (await editorState.pasteInAppJson(inAppJson)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (html != null && html.isNotEmpty) {
|
||||
await editorState.deleteSelectionIfNeeded();
|
||||
final result = await editorState.pasteHtml(html);
|
||||
if (result) {
|
||||
if (await editorState.pasteHtml(html)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (image != null && image.$2?.isNotEmpty == true) {
|
||||
final documentBloc =
|
||||
editorState.document.root.context?.read<DocumentBloc>();
|
||||
final documentId = documentBloc?.documentId;
|
||||
if (documentId == null || documentId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
await editorState.deleteSelectionIfNeeded();
|
||||
final result = await editorState.pasteImage(image.$1, image.$2!);
|
||||
final result = await editorState.pasteImage(
|
||||
image.$1,
|
||||
image.$2!,
|
||||
documentId,
|
||||
);
|
||||
if (result) {
|
||||
return;
|
||||
}
|
||||
|
||||
+19
@@ -1,5 +1,11 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
final _listTypes = [
|
||||
BulletedListBlockKeys.type,
|
||||
TodoListBlockKeys.type,
|
||||
NumberedListBlockKeys.type,
|
||||
];
|
||||
|
||||
extension PasteNodes on EditorState {
|
||||
Future<void> pasteSingleLineNode(Node insertedNode) async {
|
||||
final selection = await deleteSelectionIfNeeded();
|
||||
@@ -28,6 +34,19 @@ extension PasteNodes on EditorState {
|
||||
offset: offset,
|
||||
),
|
||||
);
|
||||
} else if (_listTypes.contains(node.type)) {
|
||||
final convertedNode = insertedNode.copyWith(type: node.type);
|
||||
final path = selection.start.path;
|
||||
transaction
|
||||
..insertNode(path, convertedNode)
|
||||
..deleteNodesAtPath(path);
|
||||
|
||||
// Set the afterSelection to the last child of the inserted node
|
||||
final lastChildPath = calculatePath(path, [convertedNode]);
|
||||
final lastChildOffset = calculateLength([convertedNode]);
|
||||
transaction.afterSelection = Selection.collapsed(
|
||||
Position(path: lastChildPath, offset: lastChildOffset),
|
||||
);
|
||||
} else if (insertedDelta != null) {
|
||||
// if the node is not empty, insert the delta from inserted node after the selection.
|
||||
transaction.insertTextDelta(node, selection.endIndex, insertedDelta);
|
||||
|
||||
+6
-2
@@ -21,7 +21,11 @@ extension PasteFromImage on EditorState {
|
||||
'gif',
|
||||
];
|
||||
|
||||
Future<bool> pasteImage(String format, Uint8List imageBytes) async {
|
||||
Future<bool> pasteImage(
|
||||
String format,
|
||||
Uint8List imageBytes,
|
||||
String documentId,
|
||||
) async {
|
||||
if (!supportedImageFormats.contains(format)) {
|
||||
return false;
|
||||
}
|
||||
@@ -56,7 +60,7 @@ extension PasteFromImage on EditorState {
|
||||
if (isLocalMode) {
|
||||
path = await saveImageToLocalStorage(copyToPath);
|
||||
} else {
|
||||
final result = await saveImageToCloudStorage(copyToPath);
|
||||
final result = await saveImageToCloudStorage(copyToPath, documentId);
|
||||
|
||||
final errorMessage = result.$2;
|
||||
|
||||
|
||||
+6
-3
@@ -91,6 +91,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
|
||||
String viewIcon = '';
|
||||
PageStyleCover? cover;
|
||||
late ViewPB view;
|
||||
late final ViewListener viewListener;
|
||||
|
||||
@override
|
||||
@@ -99,6 +100,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
final value = widget.view.icon.value;
|
||||
viewIcon = value.isNotEmpty ? value : icon ?? '';
|
||||
cover = widget.view.cover;
|
||||
view = widget.view;
|
||||
widget.node.addListener(_reload);
|
||||
viewListener = ViewListener(
|
||||
viewId: widget.view.id,
|
||||
@@ -107,6 +109,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
setState(() {
|
||||
viewIcon = p0.icon.value;
|
||||
cover = p0.cover;
|
||||
view = p0;
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -137,7 +140,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
),
|
||||
if (hasCover)
|
||||
DocumentCover(
|
||||
view: widget.view,
|
||||
view: view,
|
||||
editorState: widget.editorState,
|
||||
node: widget.node,
|
||||
coverType: coverType,
|
||||
@@ -204,7 +207,7 @@ class _DocumentCoverWidgetState extends State<DocumentCoverWidget> {
|
||||
// compatible with version > 0.5.5.
|
||||
EditorMigration.migrateCoverIfNeeded(
|
||||
widget.view,
|
||||
widget.editorState,
|
||||
attributes,
|
||||
overwrite: true,
|
||||
);
|
||||
}
|
||||
@@ -638,7 +641,7 @@ class DocumentCoverState extends State<DocumentCover> {
|
||||
details = await saveImageToLocalStorage(details);
|
||||
} else {
|
||||
// else we should save the image to cloud storage
|
||||
(details, _) = await saveImageToCloudStorage(details);
|
||||
(details, _) = await saveImageToCloudStorage(details, widget.view.id);
|
||||
}
|
||||
}
|
||||
widget.onChangeCover(type, details);
|
||||
|
||||
+6
-2
@@ -90,7 +90,7 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
UploadImageType.local,
|
||||
UploadImageType.url,
|
||||
UploadImageType.unsplash,
|
||||
UploadImageType.openAI,
|
||||
// UploadImageType.openAI,
|
||||
UploadImageType.stabilityAI,
|
||||
],
|
||||
onSelectedLocalImage: (path) {
|
||||
@@ -216,12 +216,16 @@ class ImagePlaceholderState extends State<ImagePlaceholder> {
|
||||
// don't limit the image size for local mode.
|
||||
path = await saveImageToLocalStorage(url);
|
||||
} else {
|
||||
final documentId = context.read<DocumentBloc>().documentId;
|
||||
if (documentId.isEmpty) {
|
||||
return;
|
||||
}
|
||||
// else we should save the image to cloud storage
|
||||
setState(() {
|
||||
showLoading = true;
|
||||
this.errorMessage = null;
|
||||
});
|
||||
(path, errorMessage) = await saveImageToCloudStorage(url);
|
||||
(path, errorMessage) = await saveImageToCloudStorage(url, documentId);
|
||||
setState(() {
|
||||
showLoading = false;
|
||||
this.errorMessage = errorMessage;
|
||||
|
||||
+2
-2
@@ -39,6 +39,7 @@ Future<String?> saveImageToLocalStorage(String localImagePath) async {
|
||||
|
||||
Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
|
||||
String localImagePath,
|
||||
String documentId,
|
||||
) async {
|
||||
final size = localImagePath.fileSize;
|
||||
if (size == null || size > 10 * 1024 * 1024) {
|
||||
@@ -52,8 +53,7 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
|
||||
Log.debug("Uploading image local path: $localImagePath");
|
||||
final result = await documentService.uploadFile(
|
||||
localFilePath: localImagePath,
|
||||
// TODO(lucas): replace with actual documentId
|
||||
documentId: "temp",
|
||||
documentId: documentId,
|
||||
);
|
||||
return result.fold(
|
||||
(s) async {
|
||||
|
||||
+3
-3
@@ -1,8 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -22,7 +22,7 @@ class OpenAIImageWidget extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _OpenAIImageWidgetState extends State<OpenAIImageWidget> {
|
||||
Future<FlowyResult<List<String>, OpenAIError>>? future;
|
||||
Future<FlowyResult<List<String>, AIError>>? future;
|
||||
String query = '';
|
||||
|
||||
@override
|
||||
@@ -93,7 +93,7 @@ class _OpenAIImageWidgetState extends State<OpenAIImageWidget> {
|
||||
}
|
||||
|
||||
void _search() async {
|
||||
final openAI = await getIt.getAsync<OpenAIRepository>();
|
||||
final openAI = await getIt.getAsync<AIRepository>();
|
||||
setState(() {
|
||||
future = openAI.generateImage(
|
||||
prompt: query,
|
||||
|
||||
+20
-21
@@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/open_ai_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/stability_ai_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_file_widget.dart';
|
||||
@@ -19,7 +18,7 @@ enum UploadImageType {
|
||||
url,
|
||||
unsplash,
|
||||
stabilityAI,
|
||||
openAI,
|
||||
// openAI,
|
||||
color;
|
||||
|
||||
String get description {
|
||||
@@ -30,8 +29,8 @@ enum UploadImageType {
|
||||
return LocaleKeys.document_imageBlock_embedLink_label.tr();
|
||||
case UploadImageType.unsplash:
|
||||
return LocaleKeys.document_imageBlock_unsplash_label.tr();
|
||||
case UploadImageType.openAI:
|
||||
return LocaleKeys.document_imageBlock_ai_label.tr();
|
||||
// case UploadImageType.openAI:
|
||||
// return LocaleKeys.document_imageBlock_ai_label.tr();
|
||||
case UploadImageType.stabilityAI:
|
||||
return LocaleKeys.document_imageBlock_stability_ai_label.tr();
|
||||
case UploadImageType.color:
|
||||
@@ -186,23 +185,23 @@ class _UploadImageMenuState extends State<UploadImageMenu> {
|
||||
),
|
||||
),
|
||||
);
|
||||
case UploadImageType.openAI:
|
||||
return supportOpenAI
|
||||
? Expanded(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
constraints: constraints,
|
||||
child: OpenAIImageWidget(
|
||||
onSelectNetworkImage: widget.onSelectedAIImage,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(),
|
||||
),
|
||||
);
|
||||
// case UploadImageType.openAI:
|
||||
// return supportOpenAI
|
||||
// ? Expanded(
|
||||
// child: Container(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// constraints: constraints,
|
||||
// child: OpenAIImageWidget(
|
||||
// onSelectNetworkImage: widget.onSelectedAIImage,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// : Padding(
|
||||
// padding: const EdgeInsets.all(8.0),
|
||||
// child: FlowyText(
|
||||
// LocaleKeys.document_imageBlock_pleaseInputYourOpenAIKey.tr(),
|
||||
// ),
|
||||
// );
|
||||
case UploadImageType.stabilityAI:
|
||||
return supportStabilityAI
|
||||
? Expanded(
|
||||
|
||||
+8
-1
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
@@ -25,7 +27,6 @@ import 'package:collection/collection.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:nanoid/non_secure.dart';
|
||||
|
||||
@@ -64,6 +65,12 @@ class _MentionDateBlockState extends State<MentionDateBlock> {
|
||||
late bool _includeTime = widget.includeTime;
|
||||
late DateTime? parsedDate = DateTime.tryParse(widget.date);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
mutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (parsedDate == null) {
|
||||
|
||||
+3
-4
@@ -164,18 +164,17 @@ class EditorMigration {
|
||||
// Now, the cover is stored in the view.ext.
|
||||
static void migrateCoverIfNeeded(
|
||||
ViewPB view,
|
||||
EditorState editorState, {
|
||||
Attributes attributes, {
|
||||
bool overwrite = false,
|
||||
}) async {
|
||||
if (view.extra.isNotEmpty && !overwrite) {
|
||||
return;
|
||||
}
|
||||
|
||||
final root = editorState.document.root;
|
||||
final coverType = CoverType.fromString(
|
||||
root.attributes[DocumentHeaderBlockKeys.coverType],
|
||||
attributes[DocumentHeaderBlockKeys.coverType],
|
||||
);
|
||||
final coverDetails = root.attributes[DocumentHeaderBlockKeys.coverDetails];
|
||||
final coverDetails = attributes[DocumentHeaderBlockKeys.coverDetails];
|
||||
|
||||
Map extra = {};
|
||||
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
abstract class AIRepository {
|
||||
Future<void> getStreamedCompletions({
|
||||
required String prompt,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(TextCompletionResponse response) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
bool useAction = false,
|
||||
});
|
||||
|
||||
Future<void> streamCompletion({
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(String text) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
});
|
||||
|
||||
Future<FlowyResult<List<String>, AIError>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
});
|
||||
}
|
||||
+5
-5
@@ -3,12 +3,12 @@ part 'error.freezed.dart';
|
||||
part 'error.g.dart';
|
||||
|
||||
@freezed
|
||||
class OpenAIError with _$OpenAIError {
|
||||
const factory OpenAIError({
|
||||
class AIError with _$AIError {
|
||||
const factory AIError({
|
||||
String? code,
|
||||
required String message,
|
||||
}) = _OpenAIError;
|
||||
}) = _AIError;
|
||||
|
||||
factory OpenAIError.fromJson(Map<String, Object?> json) =>
|
||||
_$OpenAIErrorFromJson(json);
|
||||
factory AIError.fromJson(Map<String, Object?> json) =>
|
||||
_$AIErrorFromJson(json);
|
||||
}
|
||||
+20
-131
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_edit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pbenum.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
@@ -25,58 +26,7 @@ enum OpenAIRequestType {
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OpenAIRepository {
|
||||
/// Get completions from GPT-3
|
||||
///
|
||||
/// [prompt] is the prompt text
|
||||
/// [suffix] is the suffix text
|
||||
/// [maxTokens] is the maximum number of tokens to generate
|
||||
/// [temperature] is the temperature of the model
|
||||
///
|
||||
Future<FlowyResult<TextCompletionResponse, OpenAIError>> getCompletions({
|
||||
required String prompt,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = .3,
|
||||
});
|
||||
|
||||
Future<void> getStreamedCompletions({
|
||||
required String prompt,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(TextCompletionResponse response) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(OpenAIError error) onError,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
bool useAction = false,
|
||||
});
|
||||
|
||||
/// Get edits from GPT-3
|
||||
///
|
||||
/// [input] is the input text
|
||||
/// [instruction] is the instruction text
|
||||
/// [temperature] is the temperature of the model
|
||||
///
|
||||
Future<FlowyResult<TextEditResponse, OpenAIError>> getEdits({
|
||||
required String input,
|
||||
required String instruction,
|
||||
double temperature = 0.3,
|
||||
});
|
||||
|
||||
/// Generate image from GPT-3
|
||||
///
|
||||
/// [prompt] is the prompt text
|
||||
/// [n] is the number of images to generate
|
||||
///
|
||||
/// the result is a list of urls
|
||||
Future<FlowyResult<List<String>, OpenAIError>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
});
|
||||
}
|
||||
|
||||
class HttpOpenAIRepository implements OpenAIRepository {
|
||||
class HttpOpenAIRepository implements AIRepository {
|
||||
const HttpOpenAIRepository({
|
||||
required this.client,
|
||||
required this.apiKey,
|
||||
@@ -90,50 +40,13 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
@override
|
||||
Future<FlowyResult<TextCompletionResponse, OpenAIError>> getCompletions({
|
||||
required String prompt,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
}) async {
|
||||
final parameters = {
|
||||
'model': 'gpt-3.5-turbo-instruct',
|
||||
'prompt': prompt,
|
||||
'suffix': suffix,
|
||||
'max_tokens': maxTokens,
|
||||
'temperature': temperature,
|
||||
'stream': false,
|
||||
};
|
||||
|
||||
final response = await client.post(
|
||||
OpenAIRequestType.textCompletion.uri,
|
||||
headers: headers,
|
||||
body: json.encode(parameters),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return FlowyResult.success(
|
||||
TextCompletionResponse.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(response.bodyBytes),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return FlowyResult.failure(
|
||||
OpenAIError.fromJson(json.decode(response.body)['error']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> getStreamedCompletions({
|
||||
required String prompt,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(TextCompletionResponse response) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(OpenAIError error) onError,
|
||||
required void Function(AIError error) onError,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
@@ -201,50 +114,14 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
} else {
|
||||
final body = await response.stream.bytesToString();
|
||||
onError(
|
||||
OpenAIError.fromJson(json.decode(body)['error']),
|
||||
AIError.fromJson(json.decode(body)['error']),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<TextEditResponse, OpenAIError>> getEdits({
|
||||
required String input,
|
||||
required String instruction,
|
||||
double temperature = 0.3,
|
||||
int n = 1,
|
||||
}) async {
|
||||
final parameters = {
|
||||
'model': 'gpt-4',
|
||||
'input': input,
|
||||
'instruction': instruction,
|
||||
'temperature': temperature,
|
||||
'n': n,
|
||||
};
|
||||
|
||||
final response = await client.post(
|
||||
OpenAIRequestType.textEdit.uri,
|
||||
headers: headers,
|
||||
body: json.encode(parameters),
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
return FlowyResult.success(
|
||||
TextEditResponse.fromJson(
|
||||
json.decode(
|
||||
utf8.decode(response.bodyBytes),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return FlowyResult.failure(
|
||||
OpenAIError.fromJson(json.decode(response.body)['error']),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<List<String>, OpenAIError>> generateImage({
|
||||
Future<FlowyResult<List<String>, AIError>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
}) async {
|
||||
@@ -273,11 +150,23 @@ class HttpOpenAIRepository implements OpenAIRepository {
|
||||
return FlowyResult.success(urls);
|
||||
} else {
|
||||
return FlowyResult.failure(
|
||||
OpenAIError.fromJson(json.decode(response.body)['error']),
|
||||
AIError.fromJson(json.decode(response.body)['error']),
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
return FlowyResult.failure(OpenAIError(message: error.toString()));
|
||||
return FlowyResult.failure(AIError(message: error.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> streamCompletion({
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(String text) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
+26
-62
@@ -1,22 +1,20 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/build_context_extension.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/text_robot.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/user/application/ai_service.dart';
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.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/style_widget/text_field.dart';
|
||||
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/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
class AutoCompletionBlockKeys {
|
||||
@@ -187,15 +185,11 @@ class _AutoCompletionBlockComponentState
|
||||
}
|
||||
|
||||
Future<void> _onGenerate() async {
|
||||
final loading = Loading(context);
|
||||
await loading.start();
|
||||
|
||||
await _updateEditingText();
|
||||
|
||||
final userProfile = await UserBackendService.getCurrentUserProfile()
|
||||
.then((value) => value.toNullable());
|
||||
if (userProfile == null) {
|
||||
await loading.stop();
|
||||
if (mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
@@ -208,34 +202,28 @@ class _AutoCompletionBlockComponentState
|
||||
|
||||
final textRobot = TextRobot(editorState: editorState);
|
||||
BarrierDialog? barrierDialog;
|
||||
final openAIRepository = HttpOpenAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: userProfile.openaiKey,
|
||||
);
|
||||
await openAIRepository.getStreamedCompletions(
|
||||
prompt: controller.text,
|
||||
final aiRepository = AppFlowyAIService();
|
||||
await aiRepository.streamCompletion(
|
||||
text: controller.text,
|
||||
completionType: CompletionTypePB.ContinueWriting,
|
||||
onStart: () async {
|
||||
await loading.stop();
|
||||
if (mounted) {
|
||||
barrierDialog = BarrierDialog(context);
|
||||
await barrierDialog?.show();
|
||||
barrierDialog?.show();
|
||||
await _makeSurePreviousNodeIsEmptyParagraphNode();
|
||||
}
|
||||
},
|
||||
onProcess: (response) async {
|
||||
if (response.choices.isNotEmpty) {
|
||||
final text = response.choices.first.text;
|
||||
await textRobot.autoInsertText(
|
||||
text,
|
||||
delay: Duration.zero,
|
||||
);
|
||||
}
|
||||
onProcess: (text) async {
|
||||
await textRobot.autoInsertText(
|
||||
text,
|
||||
delay: Duration.zero,
|
||||
);
|
||||
},
|
||||
onEnd: () async {
|
||||
await barrierDialog?.dismiss();
|
||||
barrierDialog?.dismiss();
|
||||
},
|
||||
onError: (error) async {
|
||||
await loading.stop();
|
||||
barrierDialog?.dismiss();
|
||||
if (mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
@@ -272,8 +260,6 @@ class _AutoCompletionBlockComponentState
|
||||
return;
|
||||
}
|
||||
|
||||
final loading = Loading(context);
|
||||
await loading.start();
|
||||
// clear previous response
|
||||
final selection = startSelection;
|
||||
if (selection != null) {
|
||||
@@ -292,7 +278,6 @@ class _AutoCompletionBlockComponentState
|
||||
final userProfile = await UserBackendService.getCurrentUserProfile()
|
||||
.then((value) => value.toNullable());
|
||||
if (userProfile == null) {
|
||||
await loading.stop();
|
||||
if (mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
@@ -303,28 +288,21 @@ class _AutoCompletionBlockComponentState
|
||||
return;
|
||||
}
|
||||
final textRobot = TextRobot(editorState: editorState);
|
||||
final openAIRepository = HttpOpenAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: userProfile.openaiKey,
|
||||
);
|
||||
await openAIRepository.getStreamedCompletions(
|
||||
prompt: _rewritePrompt(previousOutput),
|
||||
final aiService = AppFlowyAIService();
|
||||
await aiService.streamCompletion(
|
||||
text: _rewritePrompt(previousOutput),
|
||||
completionType: CompletionTypePB.ContinueWriting,
|
||||
onStart: () async {
|
||||
await loading.stop();
|
||||
await _makeSurePreviousNodeIsEmptyParagraphNode();
|
||||
},
|
||||
onProcess: (response) async {
|
||||
if (response.choices.isNotEmpty) {
|
||||
final text = response.choices.first.text;
|
||||
await textRobot.autoInsertText(
|
||||
text,
|
||||
delay: Duration.zero,
|
||||
);
|
||||
}
|
||||
onProcess: (text) async {
|
||||
await textRobot.autoInsertText(
|
||||
text,
|
||||
delay: Duration.zero,
|
||||
);
|
||||
},
|
||||
onEnd: () async {},
|
||||
onError: (error) async {
|
||||
await loading.stop();
|
||||
if (mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
@@ -462,23 +440,9 @@ class AutoCompletionHeader extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
|
||||
fontSize: 14,
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
onTap: () async {
|
||||
await openLearnMorePage();
|
||||
},
|
||||
),
|
||||
],
|
||||
return FlowyText.medium(
|
||||
LocaleKeys.document_plugins_autoGeneratorTitleName.tr(),
|
||||
fontSize: 14,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+32
-26
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class Loading {
|
||||
@@ -6,25 +8,27 @@ class Loading {
|
||||
BuildContext? loadingContext;
|
||||
final BuildContext context;
|
||||
|
||||
Future<void> start() async => showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingContext = context;
|
||||
return const SimpleDialog(
|
||||
elevation: 0.0,
|
||||
backgroundColor:
|
||||
Colors.transparent, // can change this to your preferred color
|
||||
children: [
|
||||
Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
void start() => unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingContext = context;
|
||||
return const SimpleDialog(
|
||||
elevation: 0.0,
|
||||
backgroundColor:
|
||||
Colors.transparent, // can change this to your preferred color
|
||||
children: [
|
||||
Center(
|
||||
child: CircularProgressIndicator(),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> stop() async {
|
||||
void stop() {
|
||||
if (loadingContext != null) {
|
||||
Navigator.of(loadingContext!).pop();
|
||||
loadingContext = null;
|
||||
@@ -38,15 +42,17 @@ class BarrierDialog {
|
||||
late BuildContext loadingContext;
|
||||
final BuildContext context;
|
||||
|
||||
Future<void> show() async => showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
loadingContext = context;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
void show() => unawaited(
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
barrierColor: Colors.transparent,
|
||||
builder: (BuildContext context) {
|
||||
loadingContext = context;
|
||||
return const SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> dismiss() async => Navigator.of(loadingContext).pop();
|
||||
void dismiss() => Navigator.of(loadingContext).pop();
|
||||
}
|
||||
+37
-66
@@ -1,18 +1,18 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/util/learn_more_action.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/ai_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/decoration.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -229,7 +229,11 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_buildHeaderWidget(context),
|
||||
FlowyText.medium(
|
||||
action.name,
|
||||
fontSize: 14,
|
||||
),
|
||||
// _buildHeaderWidget(context),
|
||||
const Space(0, 10),
|
||||
_buildResultWidget(context),
|
||||
const Space(0, 10),
|
||||
@@ -238,27 +242,6 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeaderWidget(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
'${LocaleKeys.document_plugins_openAI.tr()}: ${action.name}',
|
||||
fontSize: 14,
|
||||
),
|
||||
const Spacer(),
|
||||
FlowyButton(
|
||||
useIntrinsicWidth: true,
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_autoGeneratorLearnMore.tr(),
|
||||
),
|
||||
onTap: () async {
|
||||
await openLearnMorePage();
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResultWidget(BuildContext context) {
|
||||
final loadingWidget = Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
@@ -423,45 +406,33 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
|
||||
result = "";
|
||||
});
|
||||
}
|
||||
final openAIRepository = await getIt.getAsync<OpenAIRepository>();
|
||||
|
||||
var lines = content.split('\n\n');
|
||||
if (action == SmartEditAction.summarize) {
|
||||
lines = [lines.join('\n')];
|
||||
}
|
||||
for (var i = 0; i < lines.length; i++) {
|
||||
final element = lines[i];
|
||||
await openAIRepository.getStreamedCompletions(
|
||||
useAction: true,
|
||||
prompt: action.prompt(element),
|
||||
onStart: () async {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
},
|
||||
onProcess: (response) async {
|
||||
setState(() {
|
||||
if (response.choices.first.text != '\n') {
|
||||
result += response.choices.first.text;
|
||||
}
|
||||
});
|
||||
},
|
||||
onEnd: () async {
|
||||
setState(() {
|
||||
if (i != lines.length - 1) {
|
||||
result += '\n';
|
||||
}
|
||||
});
|
||||
},
|
||||
onError: (error) async {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
error.message,
|
||||
showCancel: true,
|
||||
);
|
||||
await _onExit();
|
||||
},
|
||||
);
|
||||
}
|
||||
final aiResitory = await getIt.getAsync<AIRepository>();
|
||||
await aiResitory.streamCompletion(
|
||||
text: content,
|
||||
completionType: completionTypeFromInt(action),
|
||||
onStart: () async {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
},
|
||||
onProcess: (text) async {
|
||||
setState(() {
|
||||
result += text;
|
||||
});
|
||||
},
|
||||
onEnd: () async {
|
||||
setState(() {
|
||||
result += '\n';
|
||||
});
|
||||
},
|
||||
onError: (error) async {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
error.message,
|
||||
showCancel: true,
|
||||
);
|
||||
await _onExit();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+9
-7
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.da
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -32,7 +33,7 @@ class SmartEditActionList extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SmartEditActionListState extends State<SmartEditActionList> {
|
||||
bool isOpenAIEnabled = false;
|
||||
bool isAIEnabled = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -40,8 +41,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
|
||||
|
||||
UserBackendService.getCurrentUserProfile().then((value) {
|
||||
setState(() {
|
||||
isOpenAIEnabled = value.fold(
|
||||
(s) => s.openaiKey.isNotEmpty,
|
||||
isAIEnabled = value.fold(
|
||||
(userProfile) =>
|
||||
userProfile.authenticator == AuthenticatorPB.AppFlowyCloud,
|
||||
(_) => false,
|
||||
);
|
||||
});
|
||||
@@ -60,9 +62,9 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
|
||||
keepEditorFocusNotifier.increase();
|
||||
return FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
tooltipText: isOpenAIEnabled
|
||||
tooltipText: isAIEnabled
|
||||
? LocaleKeys.document_plugins_smartEdit.tr()
|
||||
: LocaleKeys.document_plugins_smartEditDisabled.tr(),
|
||||
: LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||
preferBelow: false,
|
||||
icon: const Icon(
|
||||
Icons.lightbulb_outline,
|
||||
@@ -70,12 +72,12 @@ class _SmartEditActionListState extends State<SmartEditActionList> {
|
||||
color: Colors.white,
|
||||
),
|
||||
onPressed: () {
|
||||
if (isOpenAIEnabled) {
|
||||
if (isAIEnabled) {
|
||||
controller.show();
|
||||
} else {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.document_plugins_smartEditDisabled.tr(),
|
||||
LocaleKeys.document_plugins_appflowyAIEditDisabled.tr(),
|
||||
showCancel: true,
|
||||
);
|
||||
}
|
||||
|
||||
+3
-1
@@ -31,8 +31,10 @@ import 'package:image_picker/image_picker.dart';
|
||||
class PageStyleCoverImage extends StatelessWidget {
|
||||
PageStyleCoverImage({
|
||||
super.key,
|
||||
required this.documentId,
|
||||
});
|
||||
|
||||
final String documentId;
|
||||
late final ImagePicker _imagePicker = ImagePicker();
|
||||
|
||||
@override
|
||||
@@ -230,7 +232,7 @@ class PageStyleCoverImage extends StatelessWidget {
|
||||
type = PageStyleCoverImageType.localImage;
|
||||
} else {
|
||||
// else we should save the image to cloud storage
|
||||
(result, _) = await saveImageToCloudStorage(path);
|
||||
(result, _) = await saveImageToCloudStorage(path, documentId);
|
||||
type = PageStyleCoverImageType.customImage;
|
||||
}
|
||||
if (!context.mounted) {
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@ class PageStyleBottomSheet extends StatelessWidget {
|
||||
fontSize: 14.0,
|
||||
),
|
||||
const VSpace(8.0),
|
||||
PageStyleCoverImage(),
|
||||
PageStyleCoverImage(documentId: view.id),
|
||||
const VSpace(20.0),
|
||||
// layout: font size, line height and font family.
|
||||
FlowyText(
|
||||
|
||||
Whitespace-only changes.
@@ -1,246 +0,0 @@
|
||||
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/workspace/presentation/home/toast.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/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) {
|
||||
if (state.publishResult != null) {
|
||||
state.publishResult!.fold(
|
||||
(value) => showSnackBarMessage(context, 'Published successfully'),
|
||||
(error) =>
|
||||
showSnackBarMessage(context, 'Failed to publish: $error'),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return state.isPublished
|
||||
? _PublishedWidget(
|
||||
url: state.url,
|
||||
onVisitSite: () {},
|
||||
onUnPublish: () {
|
||||
context
|
||||
.read<DocumentShareBloc>()
|
||||
.add(const DocumentShareEvent.unPublish());
|
||||
},
|
||||
)
|
||||
: _UnPublishWidget(
|
||||
onPublish: () => context
|
||||
.read<DocumentShareBloc>()
|
||||
.add(const DocumentShareEvent.publish('')),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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) {},
|
||||
onSubmitted: (url) {},
|
||||
),
|
||||
const VSpace(16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildButton(
|
||||
context,
|
||||
name: LocaleKeys.shareAction_unPublish.tr(),
|
||||
borderColor: const Color(0x1E14171B),
|
||||
onTap: widget.onUnPublish,
|
||||
),
|
||||
const Spacer(),
|
||||
_buildButton(
|
||||
context,
|
||||
name: LocaleKeys.shareAction_visitSite.tr(),
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
textColor: Colors.white,
|
||||
onTap: () {
|
||||
safeLaunchUrl(controller.text);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildButton(
|
||||
BuildContext context, {
|
||||
required String name,
|
||||
Color? backgroundColor,
|
||||
Color borderColor = Colors.transparent,
|
||||
Color? textColor,
|
||||
required VoidCallback onTap,
|
||||
}) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
width: 189,
|
||||
child: FlowyButton(
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: borderColor),
|
||||
),
|
||||
text: FlowyText.regular(
|
||||
name,
|
||||
textAlign: TextAlign.center,
|
||||
color: textColor,
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
autoFocus: false,
|
||||
controller: controller,
|
||||
suffixIcon: _buildCopyLinkIcon(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCopyLinkIcon() {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+13
-36
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -14,7 +13,6 @@ 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';
|
||||
@@ -30,44 +28,23 @@ class DocumentShareButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DocumentShareBloc>(param1: view)
|
||||
..add(const DocumentShareEvent.initial()),
|
||||
create: (context) => getIt<DocumentShareBloc>(param1: view),
|
||||
child: BlocListener<DocumentShareBloc, DocumentShareState>(
|
||||
listener: (context, state) {
|
||||
if (state.isLoading == false && state.exportResult != null) {
|
||||
state.exportResult!.fold(
|
||||
(data) => _handleExportData(context, data),
|
||||
_handleExportError,
|
||||
);
|
||||
}
|
||||
state.mapOrNull(
|
||||
finish: (state) {
|
||||
state.successOrFail.fold(
|
||||
(data) => _handleExportData(context, data),
|
||||
_handleExportError,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
|
||||
builder: (context, state) {
|
||||
final shareBloc = context.read<DocumentShareBloc>();
|
||||
return SizedBox(
|
||||
height: 32.0,
|
||||
child: IntrinsicWidth(
|
||||
child: AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 422,
|
||||
),
|
||||
margin: const EdgeInsets.all(16),
|
||||
offset: const Offset(0, 8),
|
||||
popupBuilder: (context) => BlocProvider.value(
|
||||
value: shareBloc,
|
||||
child: const ShareMenu(),
|
||||
),
|
||||
child: RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
fontSize: 14.0,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (context, state) => SizedBox(
|
||||
height: 32.0,
|
||||
child: IntrinsicWidth(child: ShareActionList(view: view)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:custom_sliding_segmented_control/custom_sliding_segmented_control.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'publish_tab.dart';
|
||||
|
||||
enum ShareMenuTab {
|
||||
share,
|
||||
publish,
|
||||
exportAs;
|
||||
|
||||
static List<ShareMenuTab> supportedTabs = [
|
||||
ShareMenuTab.share,
|
||||
ShareMenuTab.publish,
|
||||
ShareMenuTab.exportAs,
|
||||
];
|
||||
|
||||
String get i18n {
|
||||
switch (this) {
|
||||
case ShareMenuTab.share:
|
||||
return 'Share';
|
||||
case ShareMenuTab.publish:
|
||||
return LocaleKeys.shareAction_publish;
|
||||
case ShareMenuTab.exportAs:
|
||||
return 'Export as';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShareMenu extends StatefulWidget {
|
||||
const ShareMenu({super.key});
|
||||
|
||||
@override
|
||||
State<ShareMenu> createState() => _ShareMenuState();
|
||||
}
|
||||
|
||||
class _ShareMenuState extends State<ShareMenu> {
|
||||
ShareMenuTab selectedTab = ShareMenuTab.publish;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final children = {
|
||||
for (final tab in ShareMenuTab.supportedTabs)
|
||||
tab: _Segment(
|
||||
title: tab.i18n.tr(),
|
||||
isSelected: selectedTab == tab,
|
||||
),
|
||||
};
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(
|
||||
height: 34,
|
||||
child: CustomSlidingSegmentedControl<ShareMenuTab>(
|
||||
initialValue: selectedTab,
|
||||
curve: Curves.linear,
|
||||
padding: 0,
|
||||
fixedWidth: 128,
|
||||
innerPadding: const EdgeInsets.all(3.0),
|
||||
children: children,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFEEF0F3),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
thumbDecoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x141F2225),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
onValueChanged: (v) {
|
||||
setState(() {
|
||||
selectedTab = v;
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
_buildTab(context),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(BuildContext context) {
|
||||
switch (selectedTab) {
|
||||
case ShareMenuTab.publish:
|
||||
return const PublishTab();
|
||||
|
||||
default:
|
||||
return const Center(
|
||||
child: FlowyText('coming soon'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Segment extends StatelessWidget {
|
||||
const _Segment({
|
||||
required this.title,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textColor = isSelected ? null : Theme.of(context).hintColor;
|
||||
return FlowyText(
|
||||
title,
|
||||
textAlign: TextAlign.center,
|
||||
color: textColor,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,13 +3,14 @@ import 'package:appflowy/core/network_monitor.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/stability_ai/stability_ai_client.dart';
|
||||
import 'package:appflowy/plugins/trash/application/prelude.dart';
|
||||
import 'package:appflowy/shared/appflowy_cache_manager.dart';
|
||||
import 'package:appflowy/shared/custom_image_cache_manager.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/appflowy_cloud_task.dart';
|
||||
import 'package:appflowy/user/application/ai_service.dart';
|
||||
import 'package:appflowy/user/application/auth/af_cloud_auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/auth/supabase_auth_service.dart';
|
||||
@@ -85,15 +86,12 @@ void _resolveCommonService(
|
||||
() => mode.isTest ? MockApplicationDataStorage() : ApplicationDataStorage(),
|
||||
);
|
||||
|
||||
getIt.registerFactoryAsync<OpenAIRepository>(
|
||||
getIt.registerFactoryAsync<AIRepository>(
|
||||
() async {
|
||||
final result = await UserBackendService.getCurrentUserProfile();
|
||||
return result.fold(
|
||||
(s) {
|
||||
return HttpOpenAIRepository(
|
||||
client: http.Client(),
|
||||
apiKey: s.openaiKey,
|
||||
);
|
||||
return AppFlowyAIService();
|
||||
},
|
||||
(e) {
|
||||
throw Exception('Failed to get user profile: ${e.msg}');
|
||||
|
||||
@@ -83,4 +83,19 @@ class WindowSizeManager {
|
||||
'${scaleFactor.clamp(minScaleFactor, maxScaleFactor)}',
|
||||
);
|
||||
}
|
||||
|
||||
/// Set the window maximized status
|
||||
Future<void> setWindowMaximized(bool isMaximized) async {
|
||||
await getIt<KeyValueStorage>()
|
||||
.set(KVKeys.windowMaximized, isMaximized.toString());
|
||||
}
|
||||
|
||||
/// Get the window maximized status
|
||||
Future<bool> getWindowMaximized() async {
|
||||
return await getIt<KeyValueStorage>().getWithFormat<bool>(
|
||||
KVKeys.windowMaximized,
|
||||
(v) => bool.tryParse(v) ?? false,
|
||||
) ??
|
||||
false;
|
||||
}
|
||||
}
|
||||
@@ -13,12 +13,9 @@ import 'package:scaled_app/scaled_app.dart';
|
||||
import 'package:window_manager/window_manager.dart';
|
||||
|
||||
class InitAppWindowTask extends LaunchTask with WindowListener {
|
||||
InitAppWindowTask({
|
||||
this.title = 'AppFlowy',
|
||||
});
|
||||
InitAppWindowTask({this.title = 'AppFlowy'});
|
||||
|
||||
final String title;
|
||||
|
||||
final windowSizeManager = WindowSizeManager();
|
||||
|
||||
@override
|
||||
@@ -48,7 +45,7 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
|
||||
final position = await windowSizeManager.getPosition();
|
||||
|
||||
if (PlatformExtension.isWindows) {
|
||||
doWhenWindowReady(() {
|
||||
doWhenWindowReady(() async {
|
||||
appWindow.minSize = windowOptions.minimumSize;
|
||||
appWindow.maxSize = windowOptions.maximumSize;
|
||||
appWindow.size = windowSize;
|
||||
@@ -58,6 +55,13 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
|
||||
}
|
||||
|
||||
appWindow.show();
|
||||
|
||||
/// on Windows we maximize the window if it was previously closed
|
||||
/// from a maximized state.
|
||||
final isMaximized = await windowSizeManager.getWindowMaximized();
|
||||
if (isMaximized) {
|
||||
appWindow.maximize();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
await windowManager.waitUntilReadyToShow(windowOptions, () async {
|
||||
@@ -78,16 +82,40 @@ class InitAppWindowTask extends LaunchTask with WindowListener {
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onWindowResize() async {
|
||||
super.onWindowResize();
|
||||
|
||||
final currentWindowSize = await windowManager.getSize();
|
||||
return windowSizeManager.setSize(currentWindowSize);
|
||||
Future<void> onWindowMaximize() async {
|
||||
super.onWindowMaximize();
|
||||
await windowSizeManager.setWindowMaximized(true);
|
||||
await windowSizeManager.setPosition(Offset.zero);
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowMaximize() async {
|
||||
super.onWindowMaximize();
|
||||
Future<void> onWindowUnmaximize() async {
|
||||
super.onWindowUnmaximize();
|
||||
await windowSizeManager.setWindowMaximized(false);
|
||||
|
||||
final position = await windowManager.getPosition();
|
||||
return windowSizeManager.setPosition(position);
|
||||
}
|
||||
|
||||
@override
|
||||
void onWindowEnterFullScreen() async {
|
||||
super.onWindowEnterFullScreen();
|
||||
await windowSizeManager.setWindowMaximized(true);
|
||||
await windowSizeManager.setPosition(Offset.zero);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onWindowLeaveFullScreen() async {
|
||||
super.onWindowLeaveFullScreen();
|
||||
await windowSizeManager.setWindowMaximized(false);
|
||||
|
||||
final position = await windowManager.getPosition();
|
||||
return windowSizeManager.setPosition(position);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> onWindowResize() async {
|
||||
super.onWindowResize();
|
||||
|
||||
final currentWindowSize = await windowManager.getSize();
|
||||
return windowSizeManager.setSize(currentWindowSize);
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:fixnum/fixnum.dart' as fixnum;
|
||||
|
||||
class AppFlowyAIService implements AIRepository {
|
||||
@override
|
||||
Future<FlowyResult<List<String>, AIError>> generateImage({
|
||||
required String prompt,
|
||||
int n = 1,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> getStreamedCompletions({
|
||||
required String prompt,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(TextCompletionResponse response) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
bool useAction = false,
|
||||
}) {
|
||||
throw UnimplementedError();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<CompletionStream> streamCompletion({
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(String text) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
}) async {
|
||||
final stream = CompletionStream(
|
||||
onStart,
|
||||
onProcess,
|
||||
onEnd,
|
||||
onError,
|
||||
);
|
||||
final payload = CompleteTextPB(
|
||||
text: text,
|
||||
completionType: completionType,
|
||||
streamPort: fixnum.Int64(stream.nativePort),
|
||||
);
|
||||
|
||||
// ignore: unawaited_futures
|
||||
ChatEventCompleteText(payload).send();
|
||||
return stream;
|
||||
}
|
||||
}
|
||||
|
||||
CompletionTypePB completionTypeFromInt(SmartEditAction action) {
|
||||
switch (action) {
|
||||
case SmartEditAction.summarize:
|
||||
return CompletionTypePB.MakeShorter;
|
||||
case SmartEditAction.fixSpelling:
|
||||
return CompletionTypePB.SpellingAndGrammar;
|
||||
case SmartEditAction.improveWriting:
|
||||
return CompletionTypePB.ImproveWriting;
|
||||
case SmartEditAction.makeItLonger:
|
||||
return CompletionTypePB.MakeLonger;
|
||||
}
|
||||
}
|
||||
|
||||
class CompletionStream {
|
||||
CompletionStream(
|
||||
Future<void> Function() onStart,
|
||||
Future<void> Function(String text) onProcess,
|
||||
Future<void> Function() onEnd,
|
||||
void Function(AIError error) onError,
|
||||
) {
|
||||
_port.handler = _controller.add;
|
||||
_subscription = _controller.stream.listen(
|
||||
(event) async {
|
||||
if (event.startsWith("start:")) {
|
||||
await onStart();
|
||||
}
|
||||
|
||||
if (event.startsWith("data:")) {
|
||||
await onProcess(event.substring(5));
|
||||
}
|
||||
|
||||
if (event.startsWith("finish:")) {
|
||||
await onEnd();
|
||||
}
|
||||
if (event.startsWith("error:")) {
|
||||
onError(AIError(message: event.substring(6)));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final RawReceivePort _port = RawReceivePort();
|
||||
final StreamController<String> _controller = StreamController.broadcast();
|
||||
late StreamSubscription<String> _subscription;
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _controller.close();
|
||||
await _subscription.cancel();
|
||||
_port.close();
|
||||
}
|
||||
|
||||
StreamSubscription<String> listen(
|
||||
void Function(String event)? onData,
|
||||
) {
|
||||
return _controller.stream.listen(onData);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/core/notification/folder_notification.dart';
|
||||
@@ -11,7 +12,6 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-notification/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/notification.pb.dart'
|
||||
as user;
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:appflowy_backend/rust_stream.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
@@ -23,6 +23,9 @@ typedef DidUpdateUserWorkspacesCallback = void Function(
|
||||
RepeatedUserWorkspacePB workspaces,
|
||||
);
|
||||
typedef UserProfileNotifyValue = FlowyResult<UserProfilePB, FlowyError>;
|
||||
typedef DidUpdateUserWorkspaceSetting = void Function(
|
||||
UseAISettingPB settings,
|
||||
);
|
||||
|
||||
class UserListener {
|
||||
UserListener({
|
||||
@@ -37,28 +40,26 @@ class UserListener {
|
||||
|
||||
/// Update notification about _all_ of the users workspaces
|
||||
///
|
||||
DidUpdateUserWorkspacesCallback? didUpdateUserWorkspaces;
|
||||
DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated;
|
||||
|
||||
/// Update notification about _one_ workspace
|
||||
///
|
||||
DidUpdateUserWorkspaceCallback? didUpdateUserWorkspace;
|
||||
DidUpdateUserWorkspaceCallback? onUserWorkspaceUpdated;
|
||||
DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated;
|
||||
|
||||
void start({
|
||||
void Function(UserProfileNotifyValue)? onProfileUpdated,
|
||||
void Function(RepeatedUserWorkspacePB)? didUpdateUserWorkspaces,
|
||||
void Function(UserWorkspacePB)? didUpdateUserWorkspace,
|
||||
DidUpdateUserWorkspacesCallback? onUserWorkspaceListUpdated,
|
||||
void Function(UserWorkspacePB)? onUserWorkspaceUpdated,
|
||||
DidUpdateUserWorkspaceSetting? onUserWorkspaceSettingUpdated,
|
||||
}) {
|
||||
if (onProfileUpdated != null) {
|
||||
_profileNotifier?.addPublishListener(onProfileUpdated);
|
||||
}
|
||||
|
||||
if (didUpdateUserWorkspaces != null) {
|
||||
this.didUpdateUserWorkspaces = didUpdateUserWorkspaces;
|
||||
}
|
||||
|
||||
if (didUpdateUserWorkspace != null) {
|
||||
this.didUpdateUserWorkspace = didUpdateUserWorkspace;
|
||||
}
|
||||
this.onUserWorkspaceListUpdated = onUserWorkspaceListUpdated;
|
||||
this.onUserWorkspaceUpdated = onUserWorkspaceUpdated;
|
||||
this.onUserWorkspaceSettingUpdated = onUserWorkspaceSettingUpdated;
|
||||
|
||||
_userParser = UserNotificationParser(
|
||||
id: _userProfile.id.toString(),
|
||||
@@ -92,13 +93,18 @@ class UserListener {
|
||||
result.map(
|
||||
(r) {
|
||||
final value = RepeatedUserWorkspacePB.fromBuffer(r);
|
||||
didUpdateUserWorkspaces?.call(value);
|
||||
onUserWorkspaceListUpdated?.call(value);
|
||||
},
|
||||
);
|
||||
break;
|
||||
case user.UserNotification.DidUpdateUserWorkspace:
|
||||
result.map(
|
||||
(r) => didUpdateUserWorkspace?.call(UserWorkspacePB.fromBuffer(r)),
|
||||
(r) => onUserWorkspaceUpdated?.call(UserWorkspacePB.fromBuffer(r)),
|
||||
);
|
||||
case user.UserNotification.DidUpdateAISetting:
|
||||
result.map(
|
||||
(r) =>
|
||||
onUserWorkspaceSettingUpdated?.call(UseAISettingPB.fromBuffer(r)),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -110,8 +116,8 @@ class UserListener {
|
||||
typedef WorkspaceSettingNotifyValue
|
||||
= FlowyResult<WorkspaceSettingPB, FlowyError>;
|
||||
|
||||
class UserWorkspaceListener {
|
||||
UserWorkspaceListener();
|
||||
class FolderListener {
|
||||
FolderListener();
|
||||
|
||||
final PublishNotifier<WorkspaceSettingNotifyValue> _settingChangedNotifier =
|
||||
PublishNotifier();
|
||||
|
||||
@@ -109,7 +109,7 @@ class UserBackendService implements IUserBackendService {
|
||||
return UserEventOpenWorkspace(payload).send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace() {
|
||||
static Future<FlowyResult<WorkspacePB, FlowyError>> getCurrentWorkspace() {
|
||||
return FolderEventReadCurrentWorkspace().send().then((result) {
|
||||
return result.fold(
|
||||
(workspace) => FlowyResult.success(workspace),
|
||||
|
||||
+11
-6
@@ -7,6 +7,7 @@ import 'package:appflowy/plugins/trash/application/trash_service.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_listener.dart';
|
||||
import 'package:appflowy/workspace/application/command_palette/search_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/trash.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -40,6 +41,7 @@ class CommandPaletteBloc
|
||||
Future<void> close() {
|
||||
_trashListener.close();
|
||||
_searchListener.stop();
|
||||
_debounceOnChanged?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -75,13 +77,17 @@ class CommandPaletteBloc
|
||||
emit(state.copyWith(query: null, isLoading: false, results: []));
|
||||
}
|
||||
},
|
||||
resultsChanged: (results, max) {
|
||||
resultsChanged: (results) {
|
||||
if (state.query != _oldQuery) {
|
||||
emit(state.copyWith(results: []));
|
||||
emit(state.copyWith(results: [], isLoading: true));
|
||||
_oldQuery = state.query;
|
||||
_messagesReceived = 0;
|
||||
}
|
||||
|
||||
if (state.query != results.query) {
|
||||
return;
|
||||
}
|
||||
|
||||
_messagesReceived++;
|
||||
|
||||
final searchResults = _filterDuplicates(results.items);
|
||||
@@ -90,7 +96,7 @@ class CommandPaletteBloc
|
||||
emit(
|
||||
state.copyWith(
|
||||
results: searchResults,
|
||||
isLoading: _messagesReceived != max,
|
||||
isLoading: _messagesReceived != results.sends.toInt(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -155,7 +161,7 @@ class CommandPaletteBloc
|
||||
void _performSearch(String value) =>
|
||||
add(CommandPaletteEvent.performSearch(search: value));
|
||||
|
||||
void _onResultsChanged(RepeatedSearchResultPB results) =>
|
||||
void _onResultsChanged(SearchResultNotificationPB results) =>
|
||||
add(CommandPaletteEvent.resultsChanged(results: results));
|
||||
}
|
||||
|
||||
@@ -168,8 +174,7 @@ class CommandPaletteEvent with _$CommandPaletteEvent {
|
||||
_PerformSearch;
|
||||
|
||||
const factory CommandPaletteEvent.resultsChanged({
|
||||
required RepeatedSearchResultPB results,
|
||||
@Default(1) int max,
|
||||
required SearchResultNotificationPB results,
|
||||
}) = _ResultsChanged;
|
||||
|
||||
const factory CommandPaletteEvent.trashChanged({
|
||||
|
||||
+7
-7
@@ -4,8 +4,7 @@ import 'dart:typed_data';
|
||||
import 'package:appflowy/core/notification/search_notification.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/notification.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/result.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-search/notification.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flowy_infra/notifier.dart';
|
||||
|
||||
@@ -23,14 +22,15 @@ class SearchListener {
|
||||
///
|
||||
final String? channel;
|
||||
|
||||
PublishNotifier<RepeatedSearchResultPB>? _updateNotifier = PublishNotifier();
|
||||
PublishNotifier<RepeatedSearchResultPB>? _updateDidCloseNotifier =
|
||||
PublishNotifier<SearchResultNotificationPB>? _updateNotifier =
|
||||
PublishNotifier();
|
||||
PublishNotifier<SearchResultNotificationPB>? _updateDidCloseNotifier =
|
||||
PublishNotifier();
|
||||
SearchNotificationListener? _listener;
|
||||
|
||||
void start({
|
||||
void Function(RepeatedSearchResultPB)? onResultsChanged,
|
||||
void Function(RepeatedSearchResultPB)? onResultsClosed,
|
||||
void Function(SearchResultNotificationPB)? onResultsChanged,
|
||||
void Function(SearchResultNotificationPB)? onResultsClosed,
|
||||
}) {
|
||||
if (onResultsChanged != null) {
|
||||
_updateNotifier?.addPublishListener(onResultsChanged);
|
||||
@@ -55,7 +55,7 @@ class SearchListener {
|
||||
case SearchNotification.DidUpdateResults:
|
||||
result.fold(
|
||||
(payload) => _updateNotifier?.value =
|
||||
RepeatedSearchResultPB.fromBuffer(payload),
|
||||
SearchResultNotificationPB.fromBuffer(payload),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
break;
|
||||
|
||||
@@ -9,12 +9,12 @@ part 'home_bloc.freezed.dart';
|
||||
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
HomeBloc(WorkspaceSettingPB workspaceSetting)
|
||||
: _workspaceListener = UserWorkspaceListener(),
|
||||
: _workspaceListener = FolderListener(),
|
||||
super(HomeState.initial(workspaceSetting)) {
|
||||
_dispatch(workspaceSetting);
|
||||
}
|
||||
|
||||
final UserWorkspaceListener _workspaceListener;
|
||||
final FolderListener _workspaceListener;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
|
||||
@@ -15,7 +15,7 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
|
||||
WorkspaceSettingPB workspaceSetting,
|
||||
AppearanceSettingsCubit appearanceSettingsCubit,
|
||||
double screenWidthPx,
|
||||
) : _listener = UserWorkspaceListener(),
|
||||
) : _listener = FolderListener(),
|
||||
_appearanceSettingsCubit = appearanceSettingsCubit,
|
||||
super(
|
||||
HomeSettingState.initial(
|
||||
@@ -27,7 +27,7 @@ class HomeSettingBloc extends Bloc<HomeSettingEvent, HomeSettingState> {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final UserWorkspaceListener _listener;
|
||||
final FolderListener _listener;
|
||||
final AppearanceSettingsCubit _appearanceSettingsCubit;
|
||||
|
||||
@override
|
||||
|
||||
@@ -13,7 +13,7 @@ part 'menu_user_bloc.freezed.dart';
|
||||
class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
|
||||
MenuUserBloc(this.userProfile)
|
||||
: _userListener = UserListener(userProfile: userProfile),
|
||||
_userWorkspaceListener = UserWorkspaceListener(),
|
||||
_userWorkspaceListener = FolderListener(),
|
||||
_userService = UserBackendService(userId: userProfile.id),
|
||||
super(MenuUserState.initial(userProfile)) {
|
||||
_dispatch();
|
||||
@@ -21,7 +21,7 @@ class MenuUserBloc extends Bloc<MenuUserEvent, MenuUserState> {
|
||||
|
||||
final UserBackendService _userService;
|
||||
final UserListener _userListener;
|
||||
final UserWorkspaceListener _userWorkspaceListener;
|
||||
final FolderListener _userWorkspaceListener;
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
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();
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
import 'package:appflowy/user/application/user_listener.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'settings_ai_bloc.freezed.dart';
|
||||
|
||||
class SettingsAIBloc extends Bloc<SettingsAIEvent, SettingsAIState> {
|
||||
SettingsAIBloc(this.userProfile)
|
||||
: _userListener = UserListener(userProfile: userProfile),
|
||||
super(SettingsAIState(userProfile: userProfile)) {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final UserListener _userListener;
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await _userListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
void _dispatch() {
|
||||
on<SettingsAIEvent>((event, emit) {
|
||||
event.when(
|
||||
started: () {
|
||||
_userListener.start(
|
||||
onProfileUpdated: _onProfileUpdated,
|
||||
onUserWorkspaceSettingUpdated: (settings) {
|
||||
if (!isClosed) {
|
||||
add(SettingsAIEvent.didLoadAISetting(settings));
|
||||
}
|
||||
},
|
||||
);
|
||||
_loadUserWorkspaceSetting();
|
||||
},
|
||||
didReceiveUserProfile: (userProfile) {
|
||||
emit(state.copyWith(userProfile: userProfile));
|
||||
},
|
||||
toggleAISearch: () {
|
||||
_updateUserWorkspaceSetting(
|
||||
disableSearchIndexing:
|
||||
!(state.aiSettings?.disableSearchIndexing ?? false),
|
||||
);
|
||||
},
|
||||
selectModel: (AIModelPB model) {
|
||||
_updateUserWorkspaceSetting(model: model);
|
||||
},
|
||||
didLoadAISetting: (UseAISettingPB settings) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
aiSettings: settings,
|
||||
enableSearchIndexing: !settings.disableSearchIndexing,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _updateUserWorkspaceSetting({
|
||||
bool? disableSearchIndexing,
|
||||
AIModelPB? model,
|
||||
}) {
|
||||
final payload = UpdateUserWorkspaceSettingPB(
|
||||
workspaceId: userProfile.workspaceId,
|
||||
);
|
||||
if (disableSearchIndexing != null) {
|
||||
payload.disableSearchIndexing = disableSearchIndexing;
|
||||
}
|
||||
if (model != null) {
|
||||
payload.aiModel = model;
|
||||
}
|
||||
UserEventUpdateWorkspaceSetting(payload).send();
|
||||
}
|
||||
|
||||
void _onProfileUpdated(
|
||||
FlowyResult<UserProfilePB, FlowyError> userProfileOrFailed,
|
||||
) =>
|
||||
userProfileOrFailed.fold(
|
||||
(profile) => add(SettingsAIEvent.didReceiveUserProfile(profile)),
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
|
||||
void _loadUserWorkspaceSetting() {
|
||||
final payload = UserWorkspaceIdPB(workspaceId: userProfile.workspaceId);
|
||||
UserEventGetWorkspaceSetting(payload).send().then((result) {
|
||||
result.fold((settings) {
|
||||
if (!isClosed) {
|
||||
add(SettingsAIEvent.didLoadAISetting(settings));
|
||||
}
|
||||
}, (err) {
|
||||
Log.error(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SettingsAIEvent with _$SettingsAIEvent {
|
||||
const factory SettingsAIEvent.started() = _Started;
|
||||
const factory SettingsAIEvent.didLoadAISetting(
|
||||
UseAISettingPB settings,
|
||||
) = _DidLoadWorkspaceSetting;
|
||||
|
||||
const factory SettingsAIEvent.toggleAISearch() = _toggleAISearch;
|
||||
|
||||
const factory SettingsAIEvent.selectModel(AIModelPB model) = _SelectAIModel;
|
||||
|
||||
const factory SettingsAIEvent.didReceiveUserProfile(
|
||||
UserProfilePB newUserProfile,
|
||||
) = _DidReceiveUserProfile;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SettingsAIState with _$SettingsAIState {
|
||||
const factory SettingsAIState({
|
||||
required UserProfilePB userProfile,
|
||||
UseAISettingPB? aiSettings,
|
||||
@Default(true) bool enableSearchIndexing,
|
||||
}) = _SettingsAIState;
|
||||
}
|
||||
+5
@@ -117,6 +117,10 @@ class SettingsPlanBloc extends Bloc<SettingsPlanEvent, SettingsPlanState> {
|
||||
);
|
||||
},
|
||||
cancelSubscription: () async {
|
||||
final newState = state
|
||||
.mapOrNull(ready: (state) => state)
|
||||
?.copyWith(downgradeProcessing: true);
|
||||
emit(newState ?? state);
|
||||
await _userService.cancelSubscription(workspaceId);
|
||||
add(const SettingsPlanEvent.started());
|
||||
},
|
||||
@@ -174,5 +178,6 @@ class SettingsPlanState with _$SettingsPlanState {
|
||||
required WorkspaceSubscriptionPB subscription,
|
||||
required BillingPortalPB? billingPortal,
|
||||
@Default(false) bool showSuccessDialog,
|
||||
@Default(false) bool downgradeProcessing,
|
||||
}) = _Ready;
|
||||
}
|
||||
+13
-1
@@ -1,8 +1,20 @@
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/workspace.pb.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
final _storageNumberFormat = NumberFormat()
|
||||
..maximumFractionDigits = 2
|
||||
..minimumFractionDigits = 0;
|
||||
|
||||
extension PresentableUsage on WorkspaceUsagePB {
|
||||
String get totalBlobInGb =>
|
||||
(totalBlobBytesLimit.toInt() / 1024 / 1024 / 1024).round().toString();
|
||||
|
||||
/// We use [NumberFormat] to format the current blob in GB.
|
||||
///
|
||||
/// Where the [totalBlobBytes] is the total blob bytes in bytes.
|
||||
/// And [NumberFormat.maximumFractionDigits] is set to 2.
|
||||
/// And [NumberFormat.minimumFractionDigits] is set to 0.
|
||||
///
|
||||
String get currentBlobInGb =>
|
||||
(totalBlobBytes.toInt() / 1024 / 1024 / 1024).round().toString();
|
||||
_storageNumberFormat.format(totalBlobBytes.toInt() / 1024 / 1024 / 1024);
|
||||
}
|
||||
+2
-1
@@ -13,12 +13,13 @@ enum SettingsPage {
|
||||
account,
|
||||
workspace,
|
||||
manageData,
|
||||
shortcuts,
|
||||
ai,
|
||||
plan,
|
||||
billing,
|
||||
// OLD
|
||||
notifications,
|
||||
cloud,
|
||||
shortcuts,
|
||||
member,
|
||||
featureFlags,
|
||||
}
|
||||
|
||||
+22
-27
@@ -4,34 +4,29 @@ import 'package:appflowy_backend/protobuf/flowy-folder/import.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
class ImportBackendService {
|
||||
static Future<FlowyResult<void, FlowyError>> importData(
|
||||
List<int> data,
|
||||
String name,
|
||||
String parentViewId,
|
||||
ImportTypePB importType,
|
||||
) async {
|
||||
final payload = ImportPB.create()
|
||||
..data = data
|
||||
..parentViewId = parentViewId
|
||||
..viewLayout = importType.toLayout()
|
||||
..name = name
|
||||
..importType = importType;
|
||||
return FolderEventImportData(payload).send();
|
||||
}
|
||||
class ImportPayload {
|
||||
ImportPayload({
|
||||
required this.name,
|
||||
required this.data,
|
||||
required this.layout,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final List<int> data;
|
||||
final ViewLayoutPB layout;
|
||||
}
|
||||
|
||||
extension on ImportTypePB {
|
||||
ViewLayoutPB toLayout() {
|
||||
switch (this) {
|
||||
case ImportTypePB.HistoryDocument:
|
||||
return ViewLayoutPB.Document;
|
||||
case ImportTypePB.HistoryDatabase ||
|
||||
ImportTypePB.CSV ||
|
||||
ImportTypePB.RawDatabase:
|
||||
return ViewLayoutPB.Grid;
|
||||
default:
|
||||
throw UnimplementedError('Unsupported import type $this');
|
||||
}
|
||||
class ImportBackendService {
|
||||
static Future<FlowyResult<void, FlowyError>> importPages(
|
||||
String parentViewId,
|
||||
List<ImportValuePayloadPB> values,
|
||||
) async {
|
||||
final request = ImportPayloadPB(
|
||||
parentViewId: parentViewId,
|
||||
values: values,
|
||||
syncAfterCreate: true,
|
||||
);
|
||||
|
||||
return FolderEventImportData(request).send();
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -22,7 +22,7 @@ class WorkspaceSettingsBloc
|
||||
|
||||
try {
|
||||
final currentWorkspace =
|
||||
await _userService!.getCurrentWorkspace().getOrThrow();
|
||||
await UserBackendService.getCurrentWorkspace().getOrThrow();
|
||||
|
||||
final workspaces =
|
||||
await _userService!.getWorkspaces().getOrThrow();
|
||||
|
||||
@@ -12,7 +12,8 @@ part 'folder_bloc.freezed.dart';
|
||||
enum FolderSpaceType {
|
||||
favorite,
|
||||
private,
|
||||
public;
|
||||
public,
|
||||
unknown;
|
||||
|
||||
ViewSectionPB get toViewSectionPB {
|
||||
switch (this) {
|
||||
@@ -21,6 +22,7 @@ enum FolderSpaceType {
|
||||
case FolderSpaceType.public:
|
||||
return ViewSectionPB.Public;
|
||||
case FolderSpaceType.favorite:
|
||||
case FolderSpaceType.unknown:
|
||||
throw UnimplementedError();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -124,6 +124,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
index: 0,
|
||||
layout: ViewLayoutPB.Document,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -216,7 +217,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
await _setSpaceExpandStatus(space, isExpanded);
|
||||
emit(state.copyWith(isExpanded: isExpanded));
|
||||
},
|
||||
createPage: (name, index) async {
|
||||
createPage: (name, layout, index) async {
|
||||
final parentViewId = state.currentSpace?.id;
|
||||
if (parentViewId == null) {
|
||||
return;
|
||||
@@ -224,7 +225,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
|
||||
final result = await ViewBackendService.createView(
|
||||
name: name,
|
||||
layoutType: ViewLayoutPB.Document,
|
||||
layoutType: layout,
|
||||
parentViewId: parentViewId,
|
||||
index: index,
|
||||
);
|
||||
@@ -291,6 +292,22 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
final nextSpace = spaces[nextIndex];
|
||||
add(SpaceEvent.open(nextSpace));
|
||||
},
|
||||
duplicate: () async {
|
||||
final currentSpace = state.currentSpace;
|
||||
if (currentSpace == null) {
|
||||
return;
|
||||
}
|
||||
emit(state.copyWith(isDuplicatingSpace: true));
|
||||
|
||||
final newSpace = await _duplicateSpace(currentSpace);
|
||||
// open the duplicated space
|
||||
if (newSpace != null) {
|
||||
add(const SpaceEvent.didReceiveSpaceUpdate());
|
||||
add(SpaceEvent.open(newSpace));
|
||||
}
|
||||
|
||||
emit(state.copyWith(isDuplicatingSpace: false));
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -298,6 +315,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
|
||||
late WorkspaceService _workspaceService;
|
||||
String? _workspaceId;
|
||||
late UserProfilePB userProfile;
|
||||
WorkspaceSectionsListener? _listener;
|
||||
|
||||
@override
|
||||
@@ -333,25 +351,22 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
SpacePermission.private => ViewSectionPB.Private,
|
||||
};
|
||||
|
||||
final extra = {
|
||||
ViewExtKeys.isSpaceKey: true,
|
||||
ViewExtKeys.spaceIconKey: icon,
|
||||
ViewExtKeys.spaceIconColorKey: iconColor,
|
||||
ViewExtKeys.spacePermissionKey: permission.index,
|
||||
ViewExtKeys.spaceCreatedAtKey: DateTime.now().millisecondsSinceEpoch,
|
||||
};
|
||||
final result = await _workspaceService.createView(
|
||||
name: name,
|
||||
viewSection: section,
|
||||
setAsCurrent: false,
|
||||
setAsCurrent: true,
|
||||
viewId: viewId,
|
||||
extra: jsonEncode(extra),
|
||||
);
|
||||
return await result.fold((space) async {
|
||||
Log.info('Space created: $space');
|
||||
final extra = {
|
||||
ViewExtKeys.isSpaceKey: true,
|
||||
ViewExtKeys.spaceIconKey: icon,
|
||||
ViewExtKeys.spaceIconColorKey: iconColor,
|
||||
ViewExtKeys.spacePermissionKey: permission.index,
|
||||
ViewExtKeys.spaceCreatedAtKey: DateTime.now().millisecondsSinceEpoch,
|
||||
};
|
||||
await ViewBackendService.updateView(
|
||||
viewId: space.id,
|
||||
extra: jsonEncode(extra),
|
||||
);
|
||||
return space;
|
||||
}, (error) {
|
||||
Log.error('Failed to create space: $error');
|
||||
@@ -389,6 +404,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
void _initial(UserProfilePB userProfile, String workspaceId) {
|
||||
_workspaceService = WorkspaceService(workspaceId: workspaceId);
|
||||
_workspaceId = workspaceId;
|
||||
this.userProfile = userProfile;
|
||||
|
||||
_listener = WorkspaceSectionsListener(
|
||||
user: userProfile,
|
||||
@@ -449,7 +465,7 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
|
||||
Future<bool> _getSpaceExpandStatus(ViewPB? space) async {
|
||||
if (space == null) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
return getIt<KeyValueStorage>().get(KVKeys.expandedViews).then((result) {
|
||||
@@ -587,6 +603,39 @@ class SpaceBloc extends Bloc<SpaceEvent, SpaceState> {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Future<ViewPB?> _duplicateSpace(ViewPB space) async {
|
||||
// if the space is not duplicated, try to create a new space
|
||||
final icon = space.icon.value.isNotEmpty
|
||||
? space.icon.value
|
||||
: builtInSpaceIcons.first;
|
||||
final iconColor = space.spaceIconColor ?? builtInSpaceColors.first;
|
||||
final newSpace = await _createSpace(
|
||||
name: '${space.name} (copy)',
|
||||
icon: icon,
|
||||
iconColor: iconColor,
|
||||
permission: space.spacePermission,
|
||||
);
|
||||
|
||||
if (newSpace == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (final view in space.childViews) {
|
||||
await ViewBackendService.duplicate(
|
||||
view: view,
|
||||
openAfterDuplicate: true,
|
||||
syncAfterDuplicate: true,
|
||||
includeChildren: true,
|
||||
parentViewId: newSpace.id,
|
||||
suffix: '',
|
||||
);
|
||||
}
|
||||
|
||||
Log.info('Space duplicated: $newSpace');
|
||||
|
||||
return newSpace;
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -606,6 +655,7 @@ class SpaceEvent with _$SpaceEvent {
|
||||
const factory SpaceEvent.rename(ViewPB space, String name) = _Rename;
|
||||
const factory SpaceEvent.changeIcon(String icon, String iconColor) =
|
||||
_ChangeIcon;
|
||||
const factory SpaceEvent.duplicate() = _Duplicate;
|
||||
const factory SpaceEvent.update({
|
||||
String? name,
|
||||
String? icon,
|
||||
@@ -616,6 +666,7 @@ class SpaceEvent with _$SpaceEvent {
|
||||
const factory SpaceEvent.expand(ViewPB space, bool isExpanded) = _Expand;
|
||||
const factory SpaceEvent.createPage({
|
||||
required String name,
|
||||
required ViewLayoutPB layout,
|
||||
int? index,
|
||||
}) = _CreatePage;
|
||||
const factory SpaceEvent.delete(ViewPB? space) = _Delete;
|
||||
@@ -638,6 +689,7 @@ class SpaceState with _$SpaceState {
|
||||
@Default(null) ViewPB? lastCreatedPage,
|
||||
FlowyResult<void, FlowyError>? createPageResult,
|
||||
@Default(false) bool shouldShowUpgradeDialog,
|
||||
@Default(false) bool isDuplicatingSpace,
|
||||
}) = _SpaceState;
|
||||
|
||||
factory SpaceState.initial() => const SpaceState();
|
||||
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'space_search_bloc.freezed.dart';
|
||||
|
||||
class SpaceSearchBloc extends Bloc<SpaceSearchEvent, SpaceSearchState> {
|
||||
SpaceSearchBloc() : super(SpaceSearchState.initial()) {
|
||||
on<SpaceSearchEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_allViews = await ViewBackendService.getAllViews().fold(
|
||||
(s) => s.items,
|
||||
(_) => <ViewPB>[],
|
||||
);
|
||||
},
|
||||
search: (query) {
|
||||
if (query.isEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
queryResults: null,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final queryResults = _allViews.where(
|
||||
(view) => view.name.toLowerCase().contains(query.toLowerCase()),
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
queryResults: queryResults.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
late final List<ViewPB> _allViews;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpaceSearchEvent with _$SpaceSearchEvent {
|
||||
const factory SpaceSearchEvent.initial() = _Initial;
|
||||
const factory SpaceSearchEvent.search(String query) = _Search;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class SpaceSearchState with _$SpaceSearchState {
|
||||
const factory SpaceSearchState({
|
||||
List<ViewPB>? queryResults,
|
||||
}) = _SpaceSearchState;
|
||||
|
||||
factory SpaceSearchState.initial() => const SpaceSearchState();
|
||||
}
|
||||
@@ -63,24 +63,6 @@ class SettingsUserViewBloc extends Bloc<SettingsUserEvent, SettingsUserState> {
|
||||
);
|
||||
});
|
||||
},
|
||||
updateUserOpenAIKey: (openAIKey) {
|
||||
_userService.updateUserProfile(openAIKey: openAIKey).then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
},
|
||||
updateUserStabilityAIKey: (stabilityAIKey) {
|
||||
_userService
|
||||
.updateUserProfile(stabilityAiKey: stabilityAIKey)
|
||||
.then((result) {
|
||||
result.fold(
|
||||
(l) => null,
|
||||
(err) => Log.error(err),
|
||||
);
|
||||
});
|
||||
},
|
||||
updateUserEmail: (String email) {
|
||||
_userService.updateUserProfile(email: email).then((result) {
|
||||
result.fold(
|
||||
@@ -127,11 +109,6 @@ class SettingsUserEvent with _$SettingsUserEvent {
|
||||
const factory SettingsUserEvent.updateUserIcon({required String iconUrl}) =
|
||||
_UpdateUserIcon;
|
||||
const factory SettingsUserEvent.removeUserIcon() = _RemoveUserIcon;
|
||||
const factory SettingsUserEvent.updateUserOpenAIKey(String openAIKey) =
|
||||
_UpdateUserOpenaiKey;
|
||||
const factory SettingsUserEvent.updateUserStabilityAIKey(
|
||||
String stabilityAIKey,
|
||||
) = _UpdateUserStabilityAIKey;
|
||||
const factory SettingsUserEvent.didReceiveUserProfile(
|
||||
UserProfilePB newUserProfile,
|
||||
) = _DidReceiveUserProfile;
|
||||
|
||||
@@ -29,9 +29,9 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
_listener.start(
|
||||
didUpdateUserWorkspaces: (workspaces) =>
|
||||
onUserWorkspaceListUpdated: (workspaces) =>
|
||||
add(UserWorkspaceEvent.updateWorkspaces(workspaces)),
|
||||
didUpdateUserWorkspace: (workspace) {
|
||||
onUserWorkspaceUpdated: (workspace) {
|
||||
// If currentWorkspace is updated, eg. Icon or Name, we should notify
|
||||
// the UI to render the updated information.
|
||||
final currentWorkspace = state.currentWorkspace;
|
||||
@@ -56,23 +56,21 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
await _userService.openWorkspace(currentWorkspace.workspaceId);
|
||||
}
|
||||
|
||||
WorkspaceMemberPB? currentWorkspaceMember;
|
||||
final workspaceMemberResult =
|
||||
await _userService.getWorkspaceMember();
|
||||
currentWorkspaceMember = workspaceMemberResult.fold(
|
||||
(s) => s,
|
||||
(e) => null,
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentWorkspace: currentWorkspace,
|
||||
workspaces: workspaces,
|
||||
isCollabWorkspaceOn: isCollabWorkspaceOn,
|
||||
currentWorkspaceMember: currentWorkspaceMember,
|
||||
actionResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
/// We wait with fetching the workspace member as it may take some time,
|
||||
/// to avoid blocking the UI from rendering (the sidebar).
|
||||
final workspaceMemberResult =
|
||||
await _userService.getWorkspaceMember();
|
||||
final workspaceMember = workspaceMemberResult.toNullable();
|
||||
emit(state.copyWith(currentWorkspaceMember: workspaceMember));
|
||||
},
|
||||
fetchWorkspaces: () async {
|
||||
final result = await _fetchWorkspaces();
|
||||
@@ -209,14 +207,6 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
(e) => state.currentWorkspace,
|
||||
);
|
||||
|
||||
WorkspaceMemberPB? currentWorkspaceMember;
|
||||
final workspaceMemberResult =
|
||||
await _userService.getWorkspaceMember();
|
||||
currentWorkspaceMember = workspaceMemberResult.fold(
|
||||
(s) => s,
|
||||
(e) => null,
|
||||
);
|
||||
|
||||
result
|
||||
..onSuccess((s) {
|
||||
Log.info(
|
||||
@@ -230,7 +220,6 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
currentWorkspace: currentWorkspace,
|
||||
currentWorkspaceMember: currentWorkspaceMember,
|
||||
actionResult: UserWorkspaceActionResult(
|
||||
actionType: UserWorkspaceActionType.open,
|
||||
isLoading: false,
|
||||
@@ -238,6 +227,14 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/// We wait with fetching the workspace member as it may take some time,
|
||||
/// to avoid blocking the UI from rendering (the sidebar).
|
||||
final workspaceMemberResult =
|
||||
await _userService.getWorkspaceMember();
|
||||
final workspaceMember = workspaceMemberResult.toNullable();
|
||||
|
||||
emit(state.copyWith(currentWorkspaceMember: workspaceMember));
|
||||
},
|
||||
renameWorkspace: (workspaceId, name) async {
|
||||
final result =
|
||||
@@ -406,7 +403,7 @@ class UserWorkspaceBloc extends Bloc<UserWorkspaceEvent, UserWorkspaceState> {
|
||||
)> _fetchWorkspaces() async {
|
||||
try {
|
||||
final currentWorkspace =
|
||||
await _userService.getCurrentWorkspace().getOrThrow();
|
||||
await UserBackendService.getCurrentWorkspace().getOrThrow();
|
||||
final workspaces = await _userService.getWorkspaces().getOrThrow();
|
||||
if (workspaces.isEmpty) {
|
||||
workspaces.add(convertWorkspacePBToUserWorkspace(currentWorkspace));
|
||||
@@ -512,6 +509,7 @@ class UserWorkspaceState with _$UserWorkspaceState {
|
||||
if (identical(this, other)) return true;
|
||||
|
||||
return other is UserWorkspaceState &&
|
||||
other.currentWorkspaceMember == currentWorkspaceMember &&
|
||||
other.currentWorkspace == currentWorkspace &&
|
||||
_deepCollectionEquality.equals(other.workspaces, workspaces) &&
|
||||
identical(other.actionResult, actionResult);
|
||||
|
||||
@@ -154,7 +154,12 @@ class ViewBloc extends Bloc<ViewEvent, ViewState> {
|
||||
);
|
||||
},
|
||||
duplicate: (e) async {
|
||||
final result = await ViewBackendService.duplicate(view: view);
|
||||
final result = await ViewBackendService.duplicate(
|
||||
view: view,
|
||||
openAfterDuplicate: true,
|
||||
syncAfterDuplicate: true,
|
||||
includeChildren: true,
|
||||
);
|
||||
emit(
|
||||
result.fold(
|
||||
(l) =>
|
||||
|
||||
@@ -133,13 +133,13 @@ extension ViewExtension on ViewPB {
|
||||
}
|
||||
}
|
||||
|
||||
FlowySvg get spaceIconSvg {
|
||||
FlowySvg? get spaceIconSvg {
|
||||
try {
|
||||
final ext = jsonDecode(extra);
|
||||
final icon = ext[ViewExtKeys.spaceIconKey];
|
||||
final color = ext[ViewExtKeys.spaceIconColorKey];
|
||||
if (icon == null || color == null) {
|
||||
return const FlowySvg(FlowySvgs.space_icon_s, blendMode: null);
|
||||
return null;
|
||||
}
|
||||
return FlowySvg(
|
||||
FlowySvgData('assets/flowy_icons/16x/$icon.svg'),
|
||||
@@ -147,7 +147,7 @@ extension ViewExtension on ViewPB {
|
||||
blendMode: BlendMode.srcOut,
|
||||
);
|
||||
} catch (e) {
|
||||
return const FlowySvg(FlowySvgs.space_icon_s, blendMode: null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -138,8 +138,28 @@ class ViewBackendService {
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> duplicate({
|
||||
required ViewPB view,
|
||||
required bool openAfterDuplicate,
|
||||
// should include children views
|
||||
required bool includeChildren,
|
||||
String? parentViewId,
|
||||
String? suffix,
|
||||
required bool syncAfterDuplicate,
|
||||
}) {
|
||||
return FolderEventDuplicateView(view).send();
|
||||
final payload = DuplicateViewPayloadPB.create()
|
||||
..viewId = view.id
|
||||
..openAfterDuplicate = openAfterDuplicate
|
||||
..includeChildren = includeChildren
|
||||
..syncAfterCreate = syncAfterDuplicate;
|
||||
|
||||
if (parentViewId != null) {
|
||||
payload.parentViewId = parentViewId;
|
||||
}
|
||||
|
||||
if (suffix != null) {
|
||||
payload.suffix = suffix;
|
||||
}
|
||||
|
||||
return FolderEventDuplicateView(payload).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<void, FlowyError>> favorite({
|
||||
@@ -271,42 +291,4 @@ 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();
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ class WorkspaceService {
|
||||
ViewLayoutPB? layout,
|
||||
bool? setAsCurrent,
|
||||
String? viewId,
|
||||
String? extra,
|
||||
}) {
|
||||
final payload = CreateViewPayloadPB.create()
|
||||
..parentViewId = workspaceId
|
||||
@@ -42,6 +43,10 @@ class WorkspaceService {
|
||||
payload.viewId = viewId;
|
||||
}
|
||||
|
||||
if (extra != null) {
|
||||
payload.extra = extra;
|
||||
}
|
||||
|
||||
return FolderEventCreateView(payload).send();
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -141,7 +141,8 @@ class CommandPaletteModal extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
],
|
||||
if (state.results.isNotEmpty) ...[
|
||||
if (state.results.isNotEmpty &&
|
||||
(state.query?.isNotEmpty ?? false)) ...[
|
||||
const Divider(height: 0),
|
||||
Flexible(
|
||||
child: SearchResultsList(
|
||||
|
||||
+17
-11
@@ -93,20 +93,25 @@ class _SearchResultTileState extends State<SearchResultTile> {
|
||||
SizedBox(width: 24, child: icon),
|
||||
const HSpace(6),
|
||||
],
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.isTrashed) ...[
|
||||
Flexible(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (widget.isTrashed) ...[
|
||||
FlowyText(
|
||||
LocaleKeys.commandPalette_fromTrashHint.tr(),
|
||||
color: AFThemeExtension.of(context)
|
||||
.textColor
|
||||
.withAlpha(175),
|
||||
fontSize: 10,
|
||||
),
|
||||
],
|
||||
FlowyText(
|
||||
LocaleKeys.commandPalette_fromTrashHint.tr(),
|
||||
color: AFThemeExtension.of(context)
|
||||
.textColor
|
||||
.withAlpha(175),
|
||||
fontSize: 10,
|
||||
widget.result.data,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
FlowyText(widget.result.data),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -142,6 +147,7 @@ class _DocumentPreview extends StatelessWidget {
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 12,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
||||
delegate: DesktopHomeScreenStackAdaptor(context),
|
||||
userProfile: userProfile,
|
||||
);
|
||||
final menu = _buildHomeSidebar(
|
||||
final sidebar = _buildHomeSidebar(
|
||||
context,
|
||||
layout: layout,
|
||||
userProfile: userProfile,
|
||||
@@ -170,7 +170,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
||||
return _layoutWidgets(
|
||||
layout: layout,
|
||||
homeStack: homeStack,
|
||||
homeMenu: menu,
|
||||
sidebar: sidebar,
|
||||
editPanel: editPanel,
|
||||
bubble: const QuestionBubble(),
|
||||
homeMenuResizer: homeMenuResizer,
|
||||
@@ -253,7 +253,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
||||
|
||||
Widget _layoutWidgets({
|
||||
required HomeLayout layout,
|
||||
required Widget homeMenu,
|
||||
required Widget sidebar,
|
||||
required Widget homeStack,
|
||||
required Widget editPanel,
|
||||
required Widget bubble,
|
||||
@@ -287,7 +287,7 @@ class DesktopHomeScreen extends StatelessWidget {
|
||||
bottom: 0,
|
||||
width: layout.editPanelWidth,
|
||||
),
|
||||
homeMenu
|
||||
sidebar
|
||||
.animatedPanelX(
|
||||
closeX: -layout.menuWidth,
|
||||
isClosed: !layout.showMenu,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
|
||||
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
||||
@@ -9,7 +11,6 @@ import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hotkey_manager/hotkey_manager.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:scaled_app/scaled_app.dart';
|
||||
@@ -136,13 +137,18 @@ class _HomeHotKeysState extends State<HomeHotKeys> {
|
||||
),
|
||||
|
||||
// Scale up/down the app
|
||||
HotKeyItem(
|
||||
hotKey: HotKey(
|
||||
KeyCode.equal,
|
||||
modifiers: [Platform.isMacOS ? KeyModifier.meta : KeyModifier.control],
|
||||
scope: HotKeyScope.inapp,
|
||||
// In some keyboards, the system returns equal as + keycode, while others may return add as + keycode, so add them both as zoom in key.
|
||||
...[KeyCode.equal, KeyCode.add].map(
|
||||
(keycode) => HotKeyItem(
|
||||
hotKey: HotKey(
|
||||
keycode,
|
||||
modifiers: [
|
||||
Platform.isMacOS ? KeyModifier.meta : KeyModifier.control,
|
||||
],
|
||||
scope: HotKeyScope.inapp,
|
||||
),
|
||||
keyDownHandler: (_) => _scaleWithStep(0.1),
|
||||
),
|
||||
keyDownHandler: (_) => _scaleWithStep(0.1),
|
||||
),
|
||||
|
||||
HotKeyItem(
|
||||
@@ -181,14 +187,12 @@ class _HomeHotKeysState extends State<HomeHotKeys> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_registerHotKeys(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
|
||||
_registerHotKeys(context);
|
||||
}
|
||||
|
||||
|
||||
+2
-71
@@ -6,12 +6,12 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_menu_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_more_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_pin_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -41,7 +41,7 @@ class FavoriteMenu extends StatelessWidget {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const VSpace(4),
|
||||
_FavoriteSearchField(
|
||||
SpaceSearchField(
|
||||
width: minWidth - 2 * _kHorizontalPadding,
|
||||
onSearch: (context, text) {
|
||||
context
|
||||
@@ -197,72 +197,3 @@ class _FavoriteGroups extends StatelessWidget {
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
class _FavoriteSearchField extends StatefulWidget {
|
||||
const _FavoriteSearchField({
|
||||
required this.width,
|
||||
required this.onSearch,
|
||||
});
|
||||
|
||||
final double width;
|
||||
final void Function(BuildContext context, String text) onSearch;
|
||||
|
||||
@override
|
||||
State<_FavoriteSearchField> createState() => _FavoriteSearchFieldState();
|
||||
}
|
||||
|
||||
class _FavoriteSearchFieldState extends State<_FavoriteSearchField> {
|
||||
final focusNode = FocusNode();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 30,
|
||||
width: widget.width,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: const BorderSide(
|
||||
width: 1.20,
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
color: Color(0xFF00BCF0),
|
||||
),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: CupertinoSearchTextField(
|
||||
onChanged: (text) => widget.onSearch(context, text),
|
||||
padding: EdgeInsets.zero,
|
||||
focusNode: focusNode,
|
||||
placeholder: LocaleKeys.search_label.tr(),
|
||||
prefixIcon: const FlowySvg(FlowySvgs.m_search_m),
|
||||
prefixInsets: const EdgeInsets.only(left: 12.0, right: 8.0),
|
||||
suffixIcon: const Icon(Icons.close),
|
||||
suffixInsets: const EdgeInsets.only(right: 8.0),
|
||||
itemSize: 16.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.transparent,
|
||||
),
|
||||
placeholderStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).hintColor,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+85
-54
@@ -1,19 +1,19 @@
|
||||
import 'dart:convert';
|
||||
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/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:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/container.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
@@ -67,10 +67,12 @@ class ImportPanel extends StatefulWidget {
|
||||
|
||||
class _ImportPanelState extends State<ImportPanel> {
|
||||
final flowyContainerFocusNode = FocusNode();
|
||||
final ValueNotifier<bool> showLoading = ValueNotifier(false);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
flowyContainerFocusNode.dispose();
|
||||
showLoading.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -87,37 +89,52 @@ class _ImportPanelState extends State<ImportPanel> {
|
||||
FlowyOverlay.pop(context);
|
||||
}
|
||||
},
|
||||
child: FlowyContainer(
|
||||
Theme.of(context).colorScheme.surface,
|
||||
height: height,
|
||||
width: width,
|
||||
child: GridView.count(
|
||||
childAspectRatio: 1 / .2,
|
||||
crossAxisCount: 2,
|
||||
children: ImportType.values
|
||||
.where((element) => element.enableOnRelease)
|
||||
.map(
|
||||
(e) => Card(
|
||||
child: FlowyButton(
|
||||
leftIcon: e.icon(context),
|
||||
leftIconSize: const Size.square(20),
|
||||
text: FlowyText.medium(
|
||||
e.toString(),
|
||||
fontSize: 15,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
child: Stack(
|
||||
children: [
|
||||
FlowyContainer(
|
||||
Theme.of(context).colorScheme.surface,
|
||||
height: height,
|
||||
width: width,
|
||||
child: GridView.count(
|
||||
childAspectRatio: 1 / .2,
|
||||
crossAxisCount: 2,
|
||||
children: ImportType.values
|
||||
.where((element) => element.enableOnRelease)
|
||||
.map(
|
||||
(e) => Card(
|
||||
child: FlowyButton(
|
||||
leftIcon: e.icon(context),
|
||||
leftIconSize: const Size.square(20),
|
||||
text: FlowyText.medium(
|
||||
e.toString(),
|
||||
fontSize: 15,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
color: Theme.of(context).colorScheme.tertiary,
|
||||
),
|
||||
onTap: () async {
|
||||
await _importFile(widget.parentViewId, e);
|
||||
if (context.mounted) {
|
||||
FlowyOverlay.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
onTap: () async {
|
||||
await _importFile(widget.parentViewId, e);
|
||||
if (context.mounted) {
|
||||
FlowyOverlay.pop(context);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
ValueListenableBuilder(
|
||||
valueListenable: showLoading,
|
||||
builder: (context, showLoading, child) {
|
||||
if (!showLoading) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -132,6 +149,10 @@ class _ImportPanelState extends State<ImportPanel> {
|
||||
return;
|
||||
}
|
||||
|
||||
showLoading.value = true;
|
||||
|
||||
final importValues = <ImportValuePayloadPB>[];
|
||||
|
||||
for (final file in result.files) {
|
||||
final path = file.path;
|
||||
if (path == null) {
|
||||
@@ -145,36 +166,40 @@ class _ImportPanelState extends State<ImportPanel> {
|
||||
case ImportType.historyDocument:
|
||||
final bytes = _documentDataFrom(importType, data);
|
||||
if (bytes != null) {
|
||||
await ImportBackendService.importData(
|
||||
bytes,
|
||||
name,
|
||||
parentViewId,
|
||||
ImportTypePB.HistoryDocument,
|
||||
importValues.add(
|
||||
ImportValuePayloadPB.create()
|
||||
..name = name
|
||||
..data = bytes
|
||||
..viewLayout = ViewLayoutPB.Document
|
||||
..importType = ImportTypePB.HistoryDocument,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ImportType.historyDatabase:
|
||||
await ImportBackendService.importData(
|
||||
utf8.encode(data),
|
||||
name,
|
||||
parentViewId,
|
||||
ImportTypePB.HistoryDatabase,
|
||||
importValues.add(
|
||||
ImportValuePayloadPB.create()
|
||||
..name = name
|
||||
..data = utf8.encode(data)
|
||||
..viewLayout = ViewLayoutPB.Grid
|
||||
..importType = ImportTypePB.HistoryDatabase,
|
||||
);
|
||||
break;
|
||||
case ImportType.databaseRawData:
|
||||
await ImportBackendService.importData(
|
||||
utf8.encode(data),
|
||||
name,
|
||||
parentViewId,
|
||||
ImportTypePB.RawDatabase,
|
||||
importValues.add(
|
||||
ImportValuePayloadPB.create()
|
||||
..name = name
|
||||
..data = utf8.encode(data)
|
||||
..viewLayout = ViewLayoutPB.Grid
|
||||
..importType = ImportTypePB.RawDatabase,
|
||||
);
|
||||
break;
|
||||
case ImportType.databaseCSV:
|
||||
await ImportBackendService.importData(
|
||||
utf8.encode(data),
|
||||
name,
|
||||
parentViewId,
|
||||
ImportTypePB.CSV,
|
||||
importValues.add(
|
||||
ImportValuePayloadPB.create()
|
||||
..name = name
|
||||
..data = utf8.encode(data)
|
||||
..viewLayout = ViewLayoutPB.Grid
|
||||
..importType = ImportTypePB.CSV,
|
||||
);
|
||||
break;
|
||||
default:
|
||||
@@ -182,6 +207,12 @@ class _ImportPanelState extends State<ImportPanel> {
|
||||
}
|
||||
}
|
||||
|
||||
await ImportBackendService.importPages(
|
||||
parentViewId,
|
||||
importValues,
|
||||
);
|
||||
|
||||
showLoading.value = false;
|
||||
widget.importCallback(importType, '', null);
|
||||
}
|
||||
}
|
||||
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
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/sidebar/space/space_search_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class MovePageMenu extends StatefulWidget {
|
||||
const MovePageMenu({
|
||||
super.key,
|
||||
required this.sourceView,
|
||||
required this.userProfile,
|
||||
required this.workspaceId,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final ViewPB sourceView;
|
||||
final UserProfilePB userProfile;
|
||||
final String workspaceId;
|
||||
final void Function(ViewPB view) onSelected;
|
||||
|
||||
@override
|
||||
State<MovePageMenu> createState() => _MovePageMenuState();
|
||||
}
|
||||
|
||||
class _MovePageMenuState extends State<MovePageMenu> {
|
||||
final isExpandedNotifier = PropertyValueNotifier(true);
|
||||
final isHoveredNotifier = ValueNotifier(true);
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
isExpandedNotifier.dispose();
|
||||
isHoveredNotifier.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) => SpaceBloc()
|
||||
..add(
|
||||
SpaceEvent.initial(
|
||||
widget.userProfile,
|
||||
widget.workspaceId,
|
||||
openFirstPage: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (context) => SpaceSearchBloc()
|
||||
..add(
|
||||
const SpaceSearchEvent.initial(),
|
||||
),
|
||||
),
|
||||
],
|
||||
child: BlocBuilder<SpaceBloc, SpaceState>(
|
||||
builder: (context, state) {
|
||||
final space = state.currentSpace;
|
||||
if (space == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Column(
|
||||
children: [
|
||||
SpaceSearchField(
|
||||
width: 240,
|
||||
onSearch: (context, value) {
|
||||
context.read<SpaceSearchBloc>().add(
|
||||
SpaceSearchEvent.search(
|
||||
value,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(10),
|
||||
BlocBuilder<SpaceSearchBloc, SpaceSearchState>(
|
||||
builder: (context, state) {
|
||||
if (state.queryResults == null) {
|
||||
return Expanded(
|
||||
child: _buildSpace(space),
|
||||
);
|
||||
}
|
||||
return Expanded(
|
||||
child: _buildGroupedViews(state.queryResults!),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildGroupedViews(List<ViewPB> views) {
|
||||
final groupedViews = views
|
||||
.where(
|
||||
(view) =>
|
||||
!_shouldIgnoreView(view, widget.sourceView) && !view.isSpace,
|
||||
)
|
||||
.toList();
|
||||
return _MovePageGroupedViews(
|
||||
views: groupedViews,
|
||||
onSelected: widget.onSelected,
|
||||
);
|
||||
}
|
||||
|
||||
Column _buildSpace(ViewPB space) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SpacePopup(
|
||||
useIntrinsicWidth: false,
|
||||
expand: true,
|
||||
height: 30,
|
||||
showCreateButton: false,
|
||||
child: CurrentSpace(
|
||||
onTapBlankArea: () {
|
||||
// move the page to current space
|
||||
widget.onSelected(space);
|
||||
},
|
||||
space: space,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
physics: const ClampingScrollPhysics(),
|
||||
child: SpacePages(
|
||||
key: ValueKey(space.id),
|
||||
space: space,
|
||||
isHovered: isHoveredNotifier,
|
||||
isExpandedNotifier: isExpandedNotifier,
|
||||
shouldIgnoreView: (view) => _shouldIgnoreView(
|
||||
view,
|
||||
widget.sourceView,
|
||||
),
|
||||
// hide the hover status and disable the editing actions
|
||||
disableSelectedStatus: true,
|
||||
// hide the ... and + buttons
|
||||
rightIconsBuilder: (context, view) => [],
|
||||
onSelected: (_, view) => widget.onSelected(view),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _MovePageGroupedViews extends StatelessWidget {
|
||||
const _MovePageGroupedViews({
|
||||
required this.views,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final List<ViewPB> views;
|
||||
final void Function(ViewPB view) onSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: views
|
||||
.map(
|
||||
(e) => ViewItem(
|
||||
key: ValueKey(e.id),
|
||||
view: e,
|
||||
spaceType: FolderSpaceType.unknown,
|
||||
level: 0,
|
||||
onSelected: (_, view) => onSelected(view),
|
||||
isFeedback: false,
|
||||
isDraggable: false,
|
||||
shouldRenderChildren: false,
|
||||
leftIconBuilder: (_, __) => const HSpace(0.0),
|
||||
rightIconsBuilder: (_, view) => [],
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
bool _shouldIgnoreView(ViewPB view, ViewPB sourceView) {
|
||||
// ignore the source view and database view, don't render it in the list.
|
||||
if (view.layout != ViewLayoutPB.Document) {
|
||||
return true;
|
||||
}
|
||||
return view.id == sourceView.id;
|
||||
}
|
||||
Loaded 100 of 514 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user