Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01747f13e8 | ||
|
|
4a5eda6eeb | ||
|
|
b5d799655a | ||
|
|
43b250ec28 | ||
|
|
20e82880ff | ||
|
|
661a0879c6 | ||
|
|
2fb18dd051 | ||
|
|
6334255e15 | ||
|
|
f66821715f | ||
|
|
620e027c3e | ||
|
|
864768b3ba | ||
|
|
a8b4f22703 | ||
|
|
432db0f6d5 | ||
|
|
e8e4162a5c | ||
|
|
241234726f | ||
|
|
e426970eed | ||
|
|
d5544af6c5 | ||
|
|
9361afd573 | ||
|
|
dc93a336d6 | ||
|
|
4e5482488f | ||
|
|
5bbf174ffd | ||
|
|
f36e3ae378 | ||
|
|
dc6349b4b1 | ||
|
|
44d8def3ce | ||
|
|
c6ad57f11d | ||
|
|
ff23165d3e | ||
|
|
253e7597c4 | ||
|
|
d2e3fdfefd | ||
|
|
a2336f5bf7 | ||
|
|
8833df1740 | ||
|
|
1524b10a8a | ||
|
|
fe0fa9b530 | ||
|
|
80afcf44c0 | ||
|
|
e500c89978 | ||
|
|
079b9888a8 | ||
|
|
0fe383e538 | ||
|
|
c006e29afc | ||
|
|
655a2b7093 | ||
|
|
23c67bcdba | ||
|
|
521fffd97c | ||
|
|
2ecc2a67a9 | ||
|
|
2d11215bb2 |
No files matched your search
@@ -1,72 +0,0 @@
|
||||
name: Deploy Web (Test)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build/test
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
SSH_PRIVATE_KEY: ${{ secrets.WEB_TEST_SSH_PRIVATE_KEY }}
|
||||
REMOTE_HOST: ${{ secrets.WEB_TEST_REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ secrets.WEB_TEST_REMOTE_USER }}
|
||||
SSL_CERTIFICATE: ${{ secrets.WEB_TEST_SSL_CERTIFICATE }}
|
||||
SSL_CERTIFICATE_KEY: ${{ secrets.WEB_TEST_SSL_CERTIFICATE_KEY }}
|
||||
ENV_FILE: test.env
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: setup pnpm
|
||||
uses: pnpm/action-setup@v2
|
||||
with:
|
||||
version: ${{ env.PNPM_VERSION }}
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_web_app/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm install
|
||||
- name: copy env file
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
cp ${{ env.ENV_FILE }} .env
|
||||
- name: test and lint
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run lint
|
||||
- name: build
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run build
|
||||
- name: generate SSL certificate
|
||||
run: |
|
||||
echo "${{ env.SSL_CERTIFICATE }}" > nginx-signed.crt
|
||||
echo "${{ env.SSL_CERTIFICATE_KEY }}" > nginx-signed.key
|
||||
- name: Deploy to EC2
|
||||
uses: easingthemes/ssh-deploy@main
|
||||
with:
|
||||
SSH_PRIVATE_KEY: ${{ env.SSH_PRIVATE_KEY }}
|
||||
ARGS: "-rlgoDzvc -i"
|
||||
SOURCE: "frontend/appflowy_web_app/dist frontend/appflowy_web_app/server.cjs frontend/appflowy_web_app/start.sh frontend/appflowy_web_app/Dockerfile frontend/appflowy_web_app/nginx.conf frontend/appflowy_web_app/.env nginx-signed.crt nginx-signed.key"
|
||||
REMOTE_HOST: ${{ env.REMOTE_HOST }}
|
||||
REMOTE_USER: ${{ env.REMOTE_USER }}
|
||||
EXCLUDE: "frontend/appflowy_web_app/dist/, frontend/appflowy_web_app/node_modules/"
|
||||
SCRIPT_AFTER: |
|
||||
docker build -t appflowy-web-app .
|
||||
docker rm -f appflowy-web-app || true
|
||||
docker run -d -p 80:80 -p 443:443 --name appflowy-web-app appflowy-web-app
|
||||
@@ -13,7 +13,7 @@ on:
|
||||
- release/*
|
||||
paths:
|
||||
- frontend/**
|
||||
types: [opened, synchronize, reopened, unlocked, ready_for_review]
|
||||
types: [ opened, synchronize, reopened, unlocked, ready_for_review ]
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
@@ -28,20 +28,35 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ macos-14 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
build-self-hosted:
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: self-hosted
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Build AppFlowy
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
|
||||
cargo make --profile development-ios-arm64-sim code_generation
|
||||
|
||||
- uses: futureware-tech/simulator-action@v3
|
||||
id: simulator-action
|
||||
with:
|
||||
model: 'iPhone 15'
|
||||
shutdown_after_job: false
|
||||
|
||||
build-macos:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: macos-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
@@ -49,8 +64,7 @@ jobs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
- name: Install Flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
@@ -59,7 +73,7 @@ jobs:
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: ${{ matrix.os }}
|
||||
prefix-key: macos-latest
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: x86_64-apple-darwin, os: macos-11, extra-build-args: "" }
|
||||
- { target: x86_64-apple-darwin, os: macos-12, extra-build-args: "" }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -233,7 +233,7 @@ jobs:
|
||||
job:
|
||||
- {
|
||||
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
|
||||
os: macos-11,
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
|
||||
@@ -8,6 +8,7 @@ on:
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- ".github/workflows/rust_ci.yaml"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -22,17 +23,49 @@ env:
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
|
||||
jobs:
|
||||
test-on-ubuntu:
|
||||
self-hosted-job:
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: self-hosted
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Checkout Appflowy Cloud
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
repository: AppFlowy-IO/AppFlowy-Cloud
|
||||
path: AppFlowy-Cloud
|
||||
|
||||
- name: Prepare Appflowy Cloud env
|
||||
working-directory: AppFlowy-Cloud
|
||||
run: |
|
||||
cp deploy.env .env
|
||||
sed -i '' 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
||||
sed -i '' 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
|
||||
|
||||
- name: Run rust-lib tests
|
||||
working-directory: frontend/rust-lib
|
||||
env:
|
||||
RUST_LOG: info
|
||||
RUST_BACKTRACE: 1
|
||||
af_cloud_test_base_url: http://localhost
|
||||
af_cloud_test_ws_url: ws://localhost/ws/v1
|
||||
af_cloud_test_gotrue_url: http://localhost/gotrue
|
||||
run: |
|
||||
DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
|
||||
|
||||
- name: rustfmt rust-lib
|
||||
run: cargo fmt --all -- --check
|
||||
working-directory: frontend/rust-lib/
|
||||
|
||||
- name: clippy rust-lib
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
working-directory: frontend/rust-lib
|
||||
|
||||
ubuntu-job:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# - name: Maximize build space
|
||||
# uses: easimon/maximize-build-space@master
|
||||
# with:
|
||||
# root-reserve-mb: 2048
|
||||
# swap-size-mb: 1024
|
||||
# remove-dotnet: 'true'
|
||||
|
||||
# the following step is required to avoid running out of space
|
||||
- name: Maximize build space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
@@ -45,23 +78,16 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
components: rustfmt, clippy
|
||||
profile: minimal
|
||||
|
||||
- name: Install prerequisites
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo install --force duckscript_cli
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: "ubuntu-latest"
|
||||
prefix-key: ${{ runner.os }}
|
||||
cache-on-failure: true
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
|
||||
@@ -74,7 +100,6 @@ jobs:
|
||||
- name: Prepare appflowy cloud env
|
||||
working-directory: AppFlowy-Cloud
|
||||
run: |
|
||||
# log level
|
||||
cp deploy.env .env
|
||||
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
|
||||
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
|
||||
@@ -106,6 +131,12 @@ jobs:
|
||||
run: cargo clippy --all-targets -- -D warnings
|
||||
working-directory: frontend/rust-lib
|
||||
|
||||
- name: "Debug: show Appflowy-Cloud container logs"
|
||||
if: failure()
|
||||
working-directory: AppFlowy-Cloud
|
||||
run: |
|
||||
docker compose logs appflowy_cloud
|
||||
|
||||
- name: Clean up Docker images
|
||||
run: |
|
||||
docker image prune -af
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
name: Tauri-CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -17,22 +18,45 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tauri-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ ubuntu-20.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
tauri-build-self-hosted:
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: self-hosted
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cd src-tauri && cargo build
|
||||
|
||||
- name: test and lint
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run lint:tauri
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_web_app
|
||||
args: "--debug"
|
||||
|
||||
tauri-build-ubuntu:
|
||||
if: github.event.pull_request.head.repo.full_name != github.repository
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
CI: true
|
||||
|
||||
steps:
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
@@ -73,14 +97,14 @@ jobs:
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
if: matrix.os == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force duckscript_cli
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
if: matrix.os == 'ubuntu-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
# Release Notes
|
||||
## Version 0.6.5 - 24/07/2024
|
||||
### New Features
|
||||
- Publish a Database to the Web
|
||||
|
||||
## Version 0.6.4 - 16/07/2024
|
||||
### New Features
|
||||
- Enhanced the message style on the AI chat page.
|
||||
- Added the ability to choose cursor color and selection color from a palette in settings page.
|
||||
### Bug Fixes
|
||||
- Optimized the performance for loading recent pages.
|
||||
- Fixed an issue where the cursor would jump randomly when typing in the document title on mobile.
|
||||
|
||||
## Version 0.6.3 - 08/07/2024
|
||||
### New Features
|
||||
- Publish a Document to the Web
|
||||
|
||||
## Version 0.6.2 - 01/07/2024
|
||||
### New Features
|
||||
- Added support for duplicating spaces.
|
||||
|
||||
Vendored
+1
-2
@@ -13,7 +13,6 @@
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
@@ -138,4 +137,4 @@
|
||||
"cwd": "${workspaceRoot}/appflowy_tauri/"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.6.2"
|
||||
APPFLOWY_VERSION = "0.6.5"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -53,7 +53,7 @@ android {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "io.appflowy.appflowy"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -8,9 +8,10 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Empty', () {
|
||||
testWidgets('toggle theme mode', (tester) async {
|
||||
testWidgets('empty test', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.wait(1000);
|
||||
});
|
||||
});
|
||||
}
|
||||
+2
-2
@@ -82,11 +82,11 @@ void main() {
|
||||
HeadingBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
importedPageEditorState.getNodeAtPath([2])!.type,
|
||||
importedPageEditorState.getNodeAtPath([1])!.type,
|
||||
HeadingBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
importedPageEditorState.getNodeAtPath([4])!.type,
|
||||
importedPageEditorState.getNodeAtPath([2])!.type,
|
||||
TableBlockKeys.type,
|
||||
);
|
||||
});
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -51,13 +51,13 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
final shareButton = find.byType(ShareActionList);
|
||||
final shareButtonState =
|
||||
tester.state(shareButton) as ShareActionListState;
|
||||
final shareButton = find.byType(ShareButton);
|
||||
final shareButtonState = tester.widget(shareButton) as ShareButton;
|
||||
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(
|
||||
context.applicationDataDirectory,
|
||||
'${shareButtonState.name}.md',
|
||||
'${shareButtonState.view.name}.md',
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -13,7 +13,9 @@ import 'util.dart';
|
||||
|
||||
extension AppFlowyAuthTest on WidgetTester {
|
||||
Future<void> tapGoogleLoginInButton() async {
|
||||
await tapButton(find.byKey(const Key('signInWithGoogleButton')));
|
||||
await tapButton(
|
||||
find.byKey(const Key('signInWithGoogleButton')),
|
||||
);
|
||||
}
|
||||
|
||||
/// Requires being on the SettingsPage.account of the SettingsDialog
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
@@ -35,6 +30,10 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'emoji.dart';
|
||||
@@ -259,7 +258,7 @@ extension CommonOperations on WidgetTester {
|
||||
/// Tap the share button above the document page.
|
||||
Future<void> tapShareButton() async {
|
||||
final shareButton = find.byWidgetPredicate(
|
||||
(widget) => widget is DocumentShareButton,
|
||||
(widget) => widget is ShareButton,
|
||||
);
|
||||
await tapButton(shareButton);
|
||||
}
|
||||
|
||||
@@ -65,6 +65,8 @@ PODS:
|
||||
- FlutterMacOS
|
||||
- permission_handler_apple (9.3.0):
|
||||
- Flutter
|
||||
- printing (1.0.0):
|
||||
- Flutter
|
||||
- ReachabilitySwift (5.0.0)
|
||||
- SDWebImage (5.14.2):
|
||||
- SDWebImage/Core (= 5.14.2)
|
||||
@@ -101,6 +103,7 @@ DEPENDENCIES:
|
||||
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
|
||||
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
|
||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||
- printing (from `.symlinks/plugins/printing/ios`)
|
||||
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
@@ -149,6 +152,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/path_provider_foundation/darwin"
|
||||
permission_handler_apple:
|
||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||
printing:
|
||||
:path: ".symlinks/plugins/printing/ios"
|
||||
share_plus:
|
||||
:path: ".symlinks/plugins/share_plus/ios"
|
||||
shared_preferences_foundation:
|
||||
@@ -170,7 +175,7 @@ SPEC CHECKSUMS:
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: fafc4fa4d01a6a9e4f772ecd190ffa525e9e2d9c
|
||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
@@ -179,6 +184,7 @@ SPEC CHECKSUMS:
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
@@ -191,4 +197,4 @@ SPEC CHECKSUMS:
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
COCOAPODS: 1.11.3
|
||||
COCOAPODS: 1.15.2
|
||||
+7
@@ -29,4 +29,11 @@ abstract class Env {
|
||||
defaultValue: '',
|
||||
)
|
||||
static const String afCloudUrl = _Env.afCloudUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: false,
|
||||
varName: 'INTERNAL_BUILD',
|
||||
defaultValue: '',
|
||||
)
|
||||
static const String internalBuild = _Env.internalBuild;
|
||||
}
|
||||
+1
-2
@@ -38,8 +38,7 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
|
||||
builder: (context, state) {
|
||||
final ids = <String>{};
|
||||
|
||||
List<ViewPB> recentViews =
|
||||
state.views.reversed.map((e) => e.item).toList();
|
||||
List<ViewPB> recentViews = state.views.map((e) => e.item).toList();
|
||||
recentViews.retainWhere((element) => ids.add(element.id));
|
||||
|
||||
// only keep the first 20 items.
|
||||
|
||||
+1
-1
@@ -49,7 +49,7 @@ class _MobileRecentSpaceState extends State<MobileRecentSpace>
|
||||
|
||||
List<SectionViewPB> _filterRecentViews(List<SectionViewPB> recentViews) {
|
||||
final ids = <String>{};
|
||||
final filteredRecentViews = recentViews.reversed.toList();
|
||||
final filteredRecentViews = recentViews.toList();
|
||||
filteredRecentViews.retainWhere((e) => ids.add(e.item.id));
|
||||
return filteredRecentViews;
|
||||
}
|
||||
|
||||
+2
-2
@@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/privacy'),
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/terms'),
|
||||
),
|
||||
if (kDebugMode)
|
||||
MobileSettingItem(
|
||||
|
||||
+47
-23
@@ -17,17 +17,23 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
required this.questionId,
|
||||
}) : super(ChatAIMessageState.initial(message)) {
|
||||
if (state.stream != null) {
|
||||
_subscription = state.stream!.listen((text) {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (text.startsWith("data:")) {
|
||||
add(ChatAIMessageEvent.newText(text.substring(5)));
|
||||
} else if (text.startsWith("error:")) {
|
||||
add(ChatAIMessageEvent.receiveError(text.substring(5)));
|
||||
}
|
||||
});
|
||||
_subscription = state.stream!.listen(
|
||||
onData: (text) {
|
||||
if (!isClosed) {
|
||||
add(ChatAIMessageEvent.updateText(text));
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (!isClosed) {
|
||||
add(ChatAIMessageEvent.receiveError(error.toString()));
|
||||
}
|
||||
},
|
||||
onAIResponseLimit: () {
|
||||
if (!isClosed) {
|
||||
add(const ChatAIMessageEvent.onAIResponseLimit());
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (state.stream!.error != null) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
@@ -42,11 +48,16 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {},
|
||||
newText: (newText) {
|
||||
emit(state.copyWith(text: state.text + newText, error: null));
|
||||
updateText: (newText) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
text: newText,
|
||||
messageState: const MessageState.ready(),
|
||||
),
|
||||
);
|
||||
},
|
||||
receiveError: (error) {
|
||||
emit(state.copyWith(error: error));
|
||||
emit(state.copyWith(messageState: MessageState.onError(error)));
|
||||
},
|
||||
retry: () {
|
||||
if (questionId is! Int64) {
|
||||
@@ -55,8 +66,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
retryState: const LoadingState.loading(),
|
||||
error: null,
|
||||
messageState: const MessageState.loading(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -82,8 +92,14 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
text: text,
|
||||
error: null,
|
||||
retryState: const LoadingState.finish(),
|
||||
messageState: const MessageState.ready(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAIResponseLimit: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageState: const MessageState.onAIResponseLimit(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -98,7 +114,7 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
return super.close();
|
||||
}
|
||||
|
||||
StreamSubscription<AnswerStreamElement>? _subscription;
|
||||
StreamSubscription<String>? _subscription;
|
||||
final String chatId;
|
||||
final Int64? questionId;
|
||||
}
|
||||
@@ -106,26 +122,34 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
@freezed
|
||||
class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
||||
const factory ChatAIMessageEvent.initial() = Initial;
|
||||
const factory ChatAIMessageEvent.newText(String text) = _NewText;
|
||||
const factory ChatAIMessageEvent.updateText(String text) = _UpdateText;
|
||||
const factory ChatAIMessageEvent.receiveError(String error) = _ReceiveError;
|
||||
const factory ChatAIMessageEvent.retry() = _Retry;
|
||||
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
||||
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatAIMessageState with _$ChatAIMessageState {
|
||||
const factory ChatAIMessageState({
|
||||
AnswerStream? stream,
|
||||
String? error,
|
||||
required String text,
|
||||
required LoadingState retryState,
|
||||
required MessageState messageState,
|
||||
}) = _ChatAIMessageState;
|
||||
|
||||
factory ChatAIMessageState.initial(dynamic text) {
|
||||
return ChatAIMessageState(
|
||||
text: text is String ? text : "",
|
||||
stream: text is AnswerStream ? text : null,
|
||||
retryState: const LoadingState.finish(),
|
||||
messageState: const MessageState.ready(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class MessageState with _$MessageState {
|
||||
const factory MessageState.onError(String error) = _Error;
|
||||
const factory MessageState.onAIResponseLimit() = _AIResponseLimit;
|
||||
const factory MessageState.ready() = _Ready;
|
||||
const factory MessageState.loading() = _Loading;
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
@@ -119,7 +120,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
final uniqueMessages = {...allMessages, ...messages}.toList()
|
||||
..sort((a, b) => b.id.compareTo(a.id));
|
||||
uniqueMessages.insertAll(0, onetimeMessages);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
messages: uniqueMessages,
|
||||
@@ -380,7 +380,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
}
|
||||
|
||||
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
|
||||
final streamMessageId = nanoid();
|
||||
final streamMessageId = (questionMessageId + 1).toString();
|
||||
|
||||
lastStreamMessageId = streamMessageId;
|
||||
|
||||
return TextMessage(
|
||||
@@ -480,7 +481,7 @@ class ChatState with _$ChatState {
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
const factory LoadingState.loading() = _Loading;
|
||||
const factory LoadingState.finish() = _Finish;
|
||||
const factory LoadingState.finish({FlowyError? error}) = _Finish;
|
||||
}
|
||||
|
||||
enum OnetimeShotType {
|
||||
@@ -524,8 +525,6 @@ OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
typedef AnswerStreamElement = String;
|
||||
|
||||
class AnswerStream {
|
||||
AnswerStream() {
|
||||
_port.handler = _controller.add;
|
||||
@@ -533,23 +532,53 @@ class AnswerStream {
|
||||
(event) {
|
||||
if (event.startsWith("data:")) {
|
||||
_hasStarted = true;
|
||||
final newText = event.substring(5);
|
||||
_text += newText;
|
||||
if (_onData != null) {
|
||||
_onData!(_text);
|
||||
}
|
||||
} else if (event.startsWith("error:")) {
|
||||
_error = event.substring(5);
|
||||
if (_onError != null) {
|
||||
_onError!(_error!);
|
||||
}
|
||||
} else if (event == "AI_RESPONSE_LIMIT") {
|
||||
if (_onAIResponseLimit != null) {
|
||||
_onAIResponseLimit!();
|
||||
}
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
if (_onEnd != null) {
|
||||
_onEnd!();
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
if (_onError != null) {
|
||||
_onError!(error.toString());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final RawReceivePort _port = RawReceivePort();
|
||||
final StreamController<AnswerStreamElement> _controller =
|
||||
StreamController.broadcast();
|
||||
late StreamSubscription<AnswerStreamElement> _subscription;
|
||||
final StreamController<String> _controller = StreamController.broadcast();
|
||||
late StreamSubscription<String> _subscription;
|
||||
bool _hasStarted = false;
|
||||
String? _error;
|
||||
String _text = "";
|
||||
|
||||
// Callbacks
|
||||
void Function(String text)? _onData;
|
||||
void Function()? _onStart;
|
||||
void Function()? _onEnd;
|
||||
void Function(String error)? _onError;
|
||||
void Function()? _onAIResponseLimit;
|
||||
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
bool get hasStarted => _hasStarted;
|
||||
String? get error => _error;
|
||||
String get text => _text;
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _controller.close();
|
||||
@@ -557,9 +586,23 @@ class AnswerStream {
|
||||
_port.close();
|
||||
}
|
||||
|
||||
StreamSubscription<AnswerStreamElement> listen(
|
||||
void Function(AnswerStreamElement event)? onData,
|
||||
) {
|
||||
return _controller.stream.listen(onData);
|
||||
StreamSubscription<String> listen({
|
||||
void Function(String text)? onData,
|
||||
void Function()? onStart,
|
||||
void Function()? onEnd,
|
||||
void Function(String error)? onError,
|
||||
void Function()? onAIResponseLimit,
|
||||
}) {
|
||||
_onData = onData;
|
||||
_onStart = onStart;
|
||||
_onEnd = onEnd;
|
||||
_onError = onError;
|
||||
_onAIResponseLimit = onAIResponseLimit;
|
||||
|
||||
if (_onStart != null) {
|
||||
_onStart!();
|
||||
}
|
||||
|
||||
return _subscription;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'chat_file_bloc.freezed.dart';
|
||||
|
||||
class ChatFileBloc extends Bloc<ChatFileEvent, ChatFileState> {
|
||||
ChatFileBloc({
|
||||
required String chatId,
|
||||
}) : listener = LocalLLMListener(),
|
||||
super(const ChatFileState()) {
|
||||
listener.start(
|
||||
chatStateCallback: (chatState) {
|
||||
if (!isClosed) {
|
||||
add(ChatFileEvent.updateChatState(chatState));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
on<ChatFileEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
final result = await ChatEventGetLocalAIChatState().send();
|
||||
result.fold(
|
||||
(chatState) {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
ChatFileEvent.updateChatState(chatState),
|
||||
);
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
Log.error(err.toString());
|
||||
},
|
||||
);
|
||||
},
|
||||
newFile: (String filePath) {
|
||||
final payload = ChatFilePB(filePath: filePath, chatId: chatId);
|
||||
ChatEventChatWithFile(payload).send();
|
||||
},
|
||||
updateChatState: (LocalAIChatPB chatState) {
|
||||
// Only user enable chat with file and the plugin is already running
|
||||
final supportChatWithFile = chatState.fileEnabled &&
|
||||
chatState.pluginState.state == RunningStatePB.Running;
|
||||
emit(
|
||||
state.copyWith(supportChatWithFile: supportChatWithFile),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
final LocalLLMListener listener;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatFileEvent with _$ChatFileEvent {
|
||||
const factory ChatFileEvent.initial() = Initial;
|
||||
const factory ChatFileEvent.newFile(String filePath) = _NewFile;
|
||||
const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) =
|
||||
_UpdateChatState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatFileState with _$ChatFileState {
|
||||
const factory ChatFileState({
|
||||
@Default(false) bool supportChatWithFile,
|
||||
}) = _ChatFileState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIChatFileIndicator with _$LocalAIChatFileIndicator {
|
||||
const factory LocalAIChatFileIndicator.ready(bool isEnabled) = _Ready;
|
||||
const factory LocalAIChatFileIndicator.loading() = _Loading;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'chat_input_bloc.freezed.dart';
|
||||
|
||||
class ChatInputBloc extends Bloc<ChatInputEvent, ChatInputState> {
|
||||
ChatInputBloc()
|
||||
: listener = LocalLLMListener(),
|
||||
super(const ChatInputState(aiType: _AppFlowyAI())) {
|
||||
listener.start(
|
||||
stateCallback: (pluginState) {
|
||||
if (!isClosed) {
|
||||
add(ChatInputEvent.updateState(pluginState));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
on<ChatInputEvent>(_handleEvent);
|
||||
}
|
||||
|
||||
final LocalLLMListener listener;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<void> _handleEvent(
|
||||
ChatInputEvent event,
|
||||
Emitter<ChatInputState> emit,
|
||||
) async {
|
||||
await event.when(
|
||||
started: () async {
|
||||
final result = await ChatEventGetLocalAIPluginState().send();
|
||||
result.fold(
|
||||
(pluginState) {
|
||||
if (!isClosed) {
|
||||
add(ChatInputEvent.updateState(pluginState));
|
||||
}
|
||||
},
|
||||
(err) => Log.error(err.toString()),
|
||||
);
|
||||
},
|
||||
updateState: (LocalAIPluginStatePB aiPluginState) {
|
||||
emit(const ChatInputState(aiType: _AppFlowyAI()));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatInputEvent with _$ChatInputEvent {
|
||||
const factory ChatInputEvent.started() = _Started;
|
||||
const factory ChatInputEvent.updateState(LocalAIPluginStatePB aiPluginState) =
|
||||
_UpdatePluginState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatInputState with _$ChatInputState {
|
||||
const factory ChatInputState({required AIType aiType}) = _ChatInputState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class AIType with _$AIType {
|
||||
const factory AIType.appflowyAI() = _AppFlowyAI;
|
||||
const factory AIType.localAI() = _LocalAI;
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
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';
|
||||
@@ -9,6 +7,7 @@ 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 {
|
||||
@@ -96,6 +95,7 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
final deletedView = notifier.isDeleted.value;
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
@@ -50,7 +53,7 @@ class AIChatUILayout {
|
||||
}
|
||||
}
|
||||
|
||||
class AIChatPage extends StatefulWidget {
|
||||
class AIChatPage extends StatelessWidget {
|
||||
const AIChatPage({
|
||||
super.key,
|
||||
required this.view,
|
||||
@@ -63,10 +66,70 @@ class AIChatPage extends StatefulWidget {
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
State<AIChatPage> createState() => _AIChatPageState();
|
||||
Widget build(BuildContext context) {
|
||||
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => ChatFileBloc(chatId: view.id.toString()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => ChatBloc(
|
||||
view: view,
|
||||
userProfile: userProfile,
|
||||
)..add(const ChatEvent.initialLoad()),
|
||||
),
|
||||
BlocProvider(create: (_) => ChatInputBloc()),
|
||||
],
|
||||
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
||||
builder: (context, state) {
|
||||
Widget child = _ChatContentPage(
|
||||
view: view,
|
||||
userProfile: userProfile,
|
||||
);
|
||||
|
||||
// If the chat supports file upload, wrap the chat content with a drop target
|
||||
if (state.supportChatWithFile) {
|
||||
child = DropTarget(
|
||||
onDragDone: (DropDoneDetails detail) async {
|
||||
for (final file in detail.files) {
|
||||
context
|
||||
.read<ChatFileBloc>()
|
||||
.add(ChatFileEvent.newFile(file.path));
|
||||
}
|
||||
},
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return child;
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
||||
fontSize: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AIChatPageState extends State<AIChatPage> {
|
||||
class _ChatContentPage extends StatefulWidget {
|
||||
const _ChatContentPage({
|
||||
required this.view,
|
||||
required this.userProfile,
|
||||
});
|
||||
|
||||
final UserProfilePB userProfile;
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
State<_ChatContentPage> createState() => _ChatContentPageState();
|
||||
}
|
||||
|
||||
class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
late types.User _user;
|
||||
|
||||
@override
|
||||
@@ -96,67 +159,61 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
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)),
|
||||
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)),
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
),
|
||||
)
|
||||
: 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,
|
||||
}) =>
|
||||
_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);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -288,31 +345,40 @@ class _AIChatPageState extends State<AIChatPage> {
|
||||
return ClipRect(
|
||||
child: Padding(
|
||||
padding: AIChatUILayout.safeAreaInsets(context),
|
||||
child: Column(
|
||||
children: [
|
||||
BlocSelector<ChatBloc, ChatState, LoadingState>(
|
||||
selector: (state) => state.streamingStatus,
|
||||
builder: (context, state) {
|
||||
return ChatInput(
|
||||
chatId: widget.view.id,
|
||||
onSendPressed: (message) =>
|
||||
onSendPressed(context, message.text),
|
||||
isStreaming: state != const LoadingState.finish(),
|
||||
onStopStreaming: () {
|
||||
context.read<ChatBloc>().add(const ChatEvent.stopStream());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(6),
|
||||
Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_aiMistakePrompt.tr(),
|
||||
fontSize: 12,
|
||||
child: BlocBuilder<ChatInputBloc, ChatInputState>(
|
||||
builder: (context, state) {
|
||||
return state.aiType.when(
|
||||
appflowyAI: () => Column(
|
||||
children: [
|
||||
BlocSelector<ChatBloc, ChatState, LoadingState>(
|
||||
selector: (state) => state.streamingStatus,
|
||||
builder: (context, state) {
|
||||
return ChatInput(
|
||||
chatId: widget.view.id,
|
||||
onSendPressed: (message) =>
|
||||
onSendPressed(context, message.text),
|
||||
isStreaming: state != const LoadingState.finish(),
|
||||
onStopStreaming: () {
|
||||
context
|
||||
.read<ChatBloc>()
|
||||
.add(const ChatEvent.stopStream());
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(6),
|
||||
Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_aiMistakePrompt.tr(),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
localAI: () => const SizedBox.shrink(),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_avatar.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_input.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_popmenu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
@@ -176,9 +182,20 @@ class CopyButton extends StatelessWidget {
|
||||
size: const Size.square(14),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: textMessage.text));
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
onPressed: () async {
|
||||
final document = customMarkdownToDocument(textMessage.text);
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
plainText: textMessage.text,
|
||||
inAppJson: jsonEncode(document.toJson()),
|
||||
),
|
||||
);
|
||||
if (context.mounted) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copiedNotification.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -49,7 +49,9 @@ class _ChatInputState extends State<ChatInput> {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (event is KeyDownEvent) {
|
||||
_handleSendPressed();
|
||||
if (!widget.isStreaming) {
|
||||
_handleSendPressed();
|
||||
}
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
@@ -78,17 +80,13 @@ class _ChatInputState extends State<ChatInput> {
|
||||
}
|
||||
|
||||
void _handleSendPressed() {
|
||||
if (widget.isStreaming) {
|
||||
widget.onStopStreaming();
|
||||
} else {
|
||||
final trimmedText = _textController.text.trim();
|
||||
if (trimmedText != '') {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
final trimmedText = _textController.text.trim();
|
||||
if (trimmedText != '') {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
|
||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
||||
_textController.clear();
|
||||
}
|
||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
||||
_textController.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -139,7 +137,6 @@ class _ChatInputState extends State<ChatInput> {
|
||||
padding: textPadding,
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
readOnly: widget.isStreaming,
|
||||
focusNode: _inputFocusNode,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
@@ -151,7 +148,6 @@ class _ChatInputState extends State<ChatInput> {
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
enabled: widget.options.enabled,
|
||||
autocorrect: widget.options.autocorrect,
|
||||
autofocus: widget.options.autofocus,
|
||||
enableSuggestions: widget.options.enableSuggestions,
|
||||
@@ -176,7 +172,10 @@ class _ChatInputState extends State<ChatInput> {
|
||||
padding: buttonPadding,
|
||||
child: AccessoryButton(
|
||||
onSendPressed: () {
|
||||
_handleSendPressed();
|
||||
if (!widget.isStreaming) {
|
||||
widget.onStopStreaming();
|
||||
_handleSendPressed();
|
||||
}
|
||||
},
|
||||
onStopStreaming: () {
|
||||
widget.onStopStreaming();
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'chat_input.dart';
|
||||
|
||||
|
||||
+373
@@ -0,0 +1,373 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_style.dart';
|
||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:markdown_widget/markdown_widget.dart';
|
||||
|
||||
import 'selectable_highlight.dart';
|
||||
|
||||
enum AIMarkdownType {
|
||||
appflowyEditor,
|
||||
markdownWidget,
|
||||
}
|
||||
|
||||
// Wrap the appflowy_editor or markdown_widget as a chat text message widget
|
||||
class AIMarkdownText extends StatelessWidget {
|
||||
const AIMarkdownText({
|
||||
super.key,
|
||||
required this.markdown,
|
||||
this.type = AIMarkdownType.appflowyEditor,
|
||||
});
|
||||
|
||||
final String markdown;
|
||||
final AIMarkdownType type;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
switch (type) {
|
||||
case AIMarkdownType.appflowyEditor:
|
||||
return _AppFlowyEditorMarkdown(markdown: markdown);
|
||||
case AIMarkdownType.markdownWidget:
|
||||
return _ThirdPartyMarkdown(markdown: markdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _AppFlowyEditorMarkdown extends StatefulWidget {
|
||||
const _AppFlowyEditorMarkdown({
|
||||
required this.markdown,
|
||||
});
|
||||
|
||||
// the text should be the markdown format
|
||||
final String markdown;
|
||||
|
||||
@override
|
||||
State<_AppFlowyEditorMarkdown> createState() =>
|
||||
_AppFlowyEditorMarkdownState();
|
||||
}
|
||||
|
||||
class _AppFlowyEditorMarkdownState extends State<_AppFlowyEditorMarkdown> {
|
||||
late EditorState editorState;
|
||||
late final styleCustomizer = EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: EdgeInsets.zero,
|
||||
);
|
||||
late final editorStyle = styleCustomizer.style().copyWith(
|
||||
// hide the cursor
|
||||
cursorColor: Colors.transparent,
|
||||
cursorWidth: 0,
|
||||
);
|
||||
late EditorScrollController scrollController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
editorState = _parseMarkdown(widget.markdown);
|
||||
scrollController = EditorScrollController(
|
||||
editorState: editorState,
|
||||
shrinkWrap: true,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _AppFlowyEditorMarkdown oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
|
||||
if (oldWidget.markdown != widget.markdown) {
|
||||
editorState.dispose();
|
||||
editorState = _parseMarkdown(widget.markdown);
|
||||
scrollController.dispose();
|
||||
scrollController = EditorScrollController(
|
||||
editorState: editorState,
|
||||
shrinkWrap: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
scrollController.dispose();
|
||||
editorState.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final blockBuilders = getEditorBuilderMap(
|
||||
context: context,
|
||||
editorState: editorState,
|
||||
styleCustomizer: styleCustomizer,
|
||||
// the editor is not editable in the chat
|
||||
editable: false,
|
||||
);
|
||||
return IntrinsicHeight(
|
||||
child: AppFlowyEditor(
|
||||
shrinkWrap: true,
|
||||
// the editor is not editable in the chat
|
||||
editable: false,
|
||||
editorStyle: editorStyle,
|
||||
editorScrollController: scrollController,
|
||||
blockComponentBuilders: blockBuilders,
|
||||
commandShortcutEvents: [customCopyCommand],
|
||||
editorState: editorState,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
EditorState _parseMarkdown(String markdown) {
|
||||
final document = customMarkdownToDocument(markdown);
|
||||
final editorState = EditorState(document: document);
|
||||
return editorState;
|
||||
}
|
||||
}
|
||||
|
||||
class _ThirdPartyMarkdown extends StatelessWidget {
|
||||
const _ThirdPartyMarkdown({
|
||||
required this.markdown,
|
||||
});
|
||||
|
||||
final String markdown;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MarkdownWidget(
|
||||
data: markdown,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
config: configFromContext(context),
|
||||
);
|
||||
}
|
||||
|
||||
MarkdownConfig configFromContext(BuildContext context) {
|
||||
return MarkdownConfig(
|
||||
configs: [
|
||||
HrConfig(color: AFThemeExtension.of(context).textColor),
|
||||
_ChatH1Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
_ChatH2Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
_ChatH3Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
H4Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
H5Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
H6Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
PreConfig(
|
||||
builder: (code, language) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 800,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
|
||||
child: SelectableHighlightView(
|
||||
code,
|
||||
language: language,
|
||||
theme: getHighlightTheme(context),
|
||||
padding: const EdgeInsets.all(14),
|
||||
textStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
PConfig(
|
||||
textStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
CodeConfig(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
BlockquoteConfig(
|
||||
sideColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
textColor: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Map<String, TextStyle> getHighlightTheme(BuildContext context) {
|
||||
return {
|
||||
'root': TextStyle(
|
||||
color: const Color(0xffabb2bf),
|
||||
backgroundColor:
|
||||
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
|
||||
),
|
||||
'comment': const TextStyle(
|
||||
color: Color(0xff5c6370),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
'quote': const TextStyle(
|
||||
color: Color(0xff5c6370),
|
||||
fontStyle: FontStyle.italic,
|
||||
),
|
||||
'doctag': const TextStyle(color: Color(0xffc678dd)),
|
||||
'keyword': const TextStyle(color: Color(0xffc678dd)),
|
||||
'formula': const TextStyle(color: Color(0xffc678dd)),
|
||||
'section': const TextStyle(color: Color(0xffe06c75)),
|
||||
'name': const TextStyle(color: Color(0xffe06c75)),
|
||||
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
|
||||
'deletion': const TextStyle(color: Color(0xffe06c75)),
|
||||
'subst': const TextStyle(color: Color(0xffe06c75)),
|
||||
'literal': const TextStyle(color: Color(0xff56b6c2)),
|
||||
'string': const TextStyle(color: Color(0xff98c379)),
|
||||
'regexp': const TextStyle(color: Color(0xff98c379)),
|
||||
'addition': const TextStyle(color: Color(0xff98c379)),
|
||||
'attribute': const TextStyle(color: Color(0xff98c379)),
|
||||
'meta-string': const TextStyle(color: Color(0xff98c379)),
|
||||
'built_in': const TextStyle(color: Color(0xffe6c07b)),
|
||||
'attr': const TextStyle(color: Color(0xffd19a66)),
|
||||
'variable': const TextStyle(color: Color(0xffd19a66)),
|
||||
'template-variable': const TextStyle(color: Color(0xffd19a66)),
|
||||
'type': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-class': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
|
||||
'number': const TextStyle(color: Color(0xffd19a66)),
|
||||
'symbol': const TextStyle(color: Color(0xff61aeee)),
|
||||
'bullet': const TextStyle(color: Color(0xff61aeee)),
|
||||
'link': const TextStyle(color: Color(0xff61aeee)),
|
||||
'meta': const TextStyle(color: Color(0xff61aeee)),
|
||||
'selector-id': const TextStyle(color: Color(0xff61aeee)),
|
||||
'title': const TextStyle(color: Color(0xff61aeee)),
|
||||
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
|
||||
'strong': const TextStyle(fontWeight: FontWeight.bold),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class _ChatH1Config extends HeadingConfig {
|
||||
const _ChatH1Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 32,
|
||||
height: 40 / 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h1.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
|
||||
///config class for h2
|
||||
class _ChatH2Config extends HeadingConfig {
|
||||
const _ChatH2Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 24,
|
||||
height: 30 / 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h2.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
|
||||
class _ChatH3Config extends HeadingConfig {
|
||||
const _ChatH3Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 24,
|
||||
height: 30 / 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h3.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
+29
-261
@@ -1,20 +1,15 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_ai_message_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_loading.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:markdown_widget/markdown_widget.dart';
|
||||
|
||||
import 'selectable_highlight.dart';
|
||||
|
||||
class ChatAITextMessageWidget extends StatelessWidget {
|
||||
const ChatAITextMessageWidget({
|
||||
@@ -42,265 +37,38 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
||||
)..add(const ChatAIMessageEvent.initial()),
|
||||
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
|
||||
builder: (context, state) {
|
||||
if (state.error != null) {
|
||||
return StreamingError(
|
||||
onRetryPressed: () {
|
||||
context.read<ChatAIMessageBloc>().add(
|
||||
const ChatAIMessageEvent.retry(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (state.retryState == const LoadingState.loading()) {
|
||||
return const ChatAILoading();
|
||||
}
|
||||
|
||||
if (state.text.isEmpty) {
|
||||
return const ChatAILoading();
|
||||
} else {
|
||||
return _textWidgetBuilder(user, context, state.text);
|
||||
}
|
||||
return state.messageState.when(
|
||||
onError: (err) {
|
||||
return StreamingError(
|
||||
onRetryPressed: () {
|
||||
context.read<ChatAIMessageBloc>().add(
|
||||
const ChatAIMessageEvent.retry(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
onAIResponseLimit: () {
|
||||
return FlowyText(
|
||||
LocaleKeys.sideBar_askOwnerToUpgradeToAIMax.tr(),
|
||||
maxLines: 10,
|
||||
lineHeight: 1.5,
|
||||
);
|
||||
},
|
||||
ready: () {
|
||||
if (state.text.isEmpty) {
|
||||
return const ChatAILoading();
|
||||
} else {
|
||||
return AIMarkdownText(markdown: state.text);
|
||||
}
|
||||
},
|
||||
loading: () {
|
||||
return const ChatAILoading();
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _textWidgetBuilder(
|
||||
User user,
|
||||
BuildContext context,
|
||||
String text,
|
||||
) {
|
||||
return MarkdownWidget(
|
||||
data: text,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
config: configFromContext(context),
|
||||
);
|
||||
}
|
||||
|
||||
MarkdownConfig configFromContext(BuildContext context) {
|
||||
return MarkdownConfig(
|
||||
configs: [
|
||||
HrConfig(color: AFThemeExtension.of(context).textColor),
|
||||
ChatH1Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
ChatH2Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
ChatH3Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
dividerColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
),
|
||||
H4Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
H5Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
H6Config(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
PreConfig(
|
||||
builder: (code, language) {
|
||||
return ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 800,
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(6.0)),
|
||||
child: SelectableHighlightView(
|
||||
code,
|
||||
language: language,
|
||||
theme: getHightlineTheme(context),
|
||||
padding: const EdgeInsets.all(14),
|
||||
textStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
PConfig(
|
||||
textStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
CodeConfig(
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
BlockquoteConfig(
|
||||
sideColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
textColor: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, TextStyle> getHightlineTheme(BuildContext context) {
|
||||
return {
|
||||
'root': TextStyle(
|
||||
color: const Color(0xffabb2bf),
|
||||
backgroundColor:
|
||||
Theme.of(context).isLightMode ? Colors.white : Colors.black38,
|
||||
),
|
||||
'comment':
|
||||
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
|
||||
'quote':
|
||||
const TextStyle(color: Color(0xff5c6370), fontStyle: FontStyle.italic),
|
||||
'doctag': const TextStyle(color: Color(0xffc678dd)),
|
||||
'keyword': const TextStyle(color: Color(0xffc678dd)),
|
||||
'formula': const TextStyle(color: Color(0xffc678dd)),
|
||||
'section': const TextStyle(color: Color(0xffe06c75)),
|
||||
'name': const TextStyle(color: Color(0xffe06c75)),
|
||||
'selector-tag': const TextStyle(color: Color(0xffe06c75)),
|
||||
'deletion': const TextStyle(color: Color(0xffe06c75)),
|
||||
'subst': const TextStyle(color: Color(0xffe06c75)),
|
||||
'literal': const TextStyle(color: Color(0xff56b6c2)),
|
||||
'string': const TextStyle(color: Color(0xff98c379)),
|
||||
'regexp': const TextStyle(color: Color(0xff98c379)),
|
||||
'addition': const TextStyle(color: Color(0xff98c379)),
|
||||
'attribute': const TextStyle(color: Color(0xff98c379)),
|
||||
'meta-string': const TextStyle(color: Color(0xff98c379)),
|
||||
'built_in': const TextStyle(color: Color(0xffe6c07b)),
|
||||
'attr': const TextStyle(color: Color(0xffd19a66)),
|
||||
'variable': const TextStyle(color: Color(0xffd19a66)),
|
||||
'template-variable': const TextStyle(color: Color(0xffd19a66)),
|
||||
'type': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-class': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-attr': const TextStyle(color: Color(0xffd19a66)),
|
||||
'selector-pseudo': const TextStyle(color: Color(0xffd19a66)),
|
||||
'number': const TextStyle(color: Color(0xffd19a66)),
|
||||
'symbol': const TextStyle(color: Color(0xff61aeee)),
|
||||
'bullet': const TextStyle(color: Color(0xff61aeee)),
|
||||
'link': const TextStyle(color: Color(0xff61aeee)),
|
||||
'meta': const TextStyle(color: Color(0xff61aeee)),
|
||||
'selector-id': const TextStyle(color: Color(0xff61aeee)),
|
||||
'title': const TextStyle(color: Color(0xff61aeee)),
|
||||
'emphasis': const TextStyle(fontStyle: FontStyle.italic),
|
||||
'strong': const TextStyle(fontWeight: FontWeight.bold),
|
||||
};
|
||||
}
|
||||
|
||||
class ChatH1Config extends HeadingConfig {
|
||||
const ChatH1Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 32,
|
||||
height: 40 / 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h1.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
|
||||
///config class for h2
|
||||
class ChatH2Config extends HeadingConfig {
|
||||
const ChatH2Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 24,
|
||||
height: 30 / 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h2.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
|
||||
class ChatH3Config extends HeadingConfig {
|
||||
const ChatH3Config({
|
||||
this.style = const TextStyle(
|
||||
fontSize: 24,
|
||||
height: 30 / 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
required this.dividerColor,
|
||||
});
|
||||
|
||||
@override
|
||||
final TextStyle style;
|
||||
final Color dividerColor;
|
||||
|
||||
@override
|
||||
String get tag => MarkdownTag.h3.name;
|
||||
|
||||
@override
|
||||
HeadingDivider? get divider => HeadingDivider(
|
||||
space: 10,
|
||||
color: dividerColor,
|
||||
height: 10,
|
||||
);
|
||||
}
|
||||
|
||||
class StreamingError extends StatelessWidget {
|
||||
|
||||
-1
@@ -52,7 +52,6 @@ class TextMessageText extends StatelessWidget {
|
||||
text,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
lineHeight: 1.5,
|
||||
maxLines: null,
|
||||
selectable: true,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
|
||||
@@ -54,6 +54,7 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) =>
|
||||
const BlankPage();
|
||||
|
||||
|
||||
+11
-6
@@ -1,7 +1,5 @@
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/row/row_cache.dart';
|
||||
import 'package:appflowy/plugins/database/application/setting/setting_listener.dart';
|
||||
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
|
||||
@@ -19,9 +17,9 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../setting/setting_service.dart';
|
||||
|
||||
import 'field_info.dart';
|
||||
|
||||
class _GridFieldNotifier extends ChangeNotifier {
|
||||
@@ -76,7 +74,6 @@ typedef OnReceiveFields = void Function(List<FieldInfo>);
|
||||
typedef OnReceiveFilters = void Function(List<FilterInfo>);
|
||||
typedef OnReceiveSorts = void Function(List<SortInfo>);
|
||||
|
||||
|
||||
class FieldController {
|
||||
FieldController({required this.viewId})
|
||||
: _fieldListener = FieldsListener(viewId: viewId),
|
||||
@@ -446,9 +443,13 @@ class FieldController {
|
||||
|
||||
/// Listen for field setting changes in the backend.
|
||||
void _listenOnFieldSettingsChanged() {
|
||||
FieldInfo updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
|
||||
FieldInfo? updateFieldSettings(FieldSettingsPB updatedFieldSettings) {
|
||||
final List<FieldInfo> newFields = fieldInfos;
|
||||
FieldInfo updatedField = newFields[0];
|
||||
var updatedField = newFields.firstOrNull;
|
||||
|
||||
if (updatedField == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final index = newFields
|
||||
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
|
||||
@@ -470,6 +471,10 @@ class FieldController {
|
||||
result.fold(
|
||||
(fieldSettings) {
|
||||
final updatedFieldInfo = updateFieldSettings(fieldSettings);
|
||||
if (updatedFieldInfo == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final listener in _updatedFieldCallbacks.values) {
|
||||
listener([updatedFieldInfo]);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class DatabaseTabBarBloc
|
||||
_createLinkedView(layout.layoutType, name ?? layout.layoutName);
|
||||
},
|
||||
deleteView: (String viewId) async {
|
||||
final result = await ViewBackendService.delete(viewId: viewId);
|
||||
final result = await ViewBackendService.deleteView(viewId: viewId);
|
||||
result.fold(
|
||||
(l) {},
|
||||
(r) => Log.error(r),
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/widgets/calculations/calculations_row.dart';
|
||||
@@ -9,6 +6,7 @@ import 'package:appflowy/plugins/database/tab_bar/desktop/setting_menu.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/action_navigation_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/action_navigation/navigation_action.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
@@ -16,6 +14,7 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/scrolling/styled_scrollview.dart';
|
||||
import 'package:flowy_infra_ui/widget/error_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:linked_scroll_controller/linked_scroll_controller.dart';
|
||||
|
||||
@@ -24,7 +23,6 @@ import '../../application/row/row_controller.dart';
|
||||
import '../../tab_bar/tab_bar_view.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
import '../application/grid_bloc.dart';
|
||||
|
||||
import 'grid_scroll.dart';
|
||||
import 'layout/layout.dart';
|
||||
import 'layout/sizes.dart';
|
||||
@@ -504,7 +502,10 @@ class _PositionedCalculationsRowState
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: GridSize.horizontalHeaderPadding + 40),
|
||||
margin: EdgeInsets.only(
|
||||
left:
|
||||
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
|
||||
),
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).canvasColor,
|
||||
|
||||
+4
-1
@@ -4,6 +4,7 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar
|
||||
import 'package:appflowy/plugins/database/domain/field_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/grid_header_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -139,7 +140,9 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
}
|
||||
|
||||
Widget _cellLeading() {
|
||||
return SizedBox(width: GridSize.horizontalHeaderPadding + 40);
|
||||
return SizedBox(
|
||||
width: context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-4
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import "package:appflowy/generated/locale_keys.g.dart";
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
@@ -7,18 +5,19 @@ import 'package:appflowy/plugins/database/application/field/field_controller.dar
|
||||
import 'package:appflowy/plugins/database/application/row/row_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/row/row_service.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/row/row_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.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 'package:provider/provider.dart';
|
||||
|
||||
import '../../../../widgets/row/accessory/cell_accessory.dart';
|
||||
import '../../../../widgets/row/cells/cell_container.dart';
|
||||
import '../../layout/sizes.dart';
|
||||
|
||||
import 'action.dart';
|
||||
|
||||
class GridRow extends StatefulWidget {
|
||||
@@ -112,7 +111,9 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
child: Consumer<RegionStateNotifier>(
|
||||
builder: (context, state, _) {
|
||||
return SizedBox(
|
||||
width: GridSize.horizontalHeaderPadding + 40,
|
||||
width: context
|
||||
.read<DatabasePluginWidgetBuilderSize>()
|
||||
.horizontalPadding,
|
||||
child: state.onEnter ? _activeWidget() : null,
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
@@ -17,14 +17,17 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'tab_bar_add_button.dart';
|
||||
|
||||
class TabBarHeader extends StatelessWidget {
|
||||
const TabBarHeader({super.key});
|
||||
const TabBarHeader({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 30,
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: GridSize.horizontalHeaderPadding + 40,
|
||||
horizontal:
|
||||
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/share_button.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
@@ -16,6 +17,7 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'desktop/tab_bar_header.dart';
|
||||
import 'mobile/mobile_tab_bar_header.dart';
|
||||
@@ -219,6 +221,16 @@ class DatabaseTabBarViewPlugin extends Plugin {
|
||||
}
|
||||
}
|
||||
|
||||
const kDatabasePluginWidgetBuilderHorizontalPadding = 'horizontal_padding';
|
||||
|
||||
class DatabasePluginWidgetBuilderSize {
|
||||
const DatabasePluginWidgetBuilderSize({
|
||||
required this.horizontalPadding,
|
||||
});
|
||||
|
||||
final double horizontalPadding;
|
||||
}
|
||||
|
||||
class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
DatabasePluginWidgetBuilder({
|
||||
required this.bloc,
|
||||
@@ -244,6 +256,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
final deletedView = notifier.isDeleted.value;
|
||||
@@ -252,11 +265,20 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
}
|
||||
});
|
||||
|
||||
return DatabaseTabBarView(
|
||||
key: ValueKey(notifier.view.id),
|
||||
view: notifier.view,
|
||||
shrinkWrap: shrinkWrap,
|
||||
initialRowId: initialRowId,
|
||||
final horizontalPadding =
|
||||
data?[kDatabasePluginWidgetBuilderHorizontalPadding] as double? ??
|
||||
GridSize.horizontalHeaderPadding + 40;
|
||||
|
||||
return Provider(
|
||||
create: (context) => DatabasePluginWidgetBuilderSize(
|
||||
horizontalPadding: horizontalPadding,
|
||||
),
|
||||
child: DatabaseTabBarView(
|
||||
key: ValueKey(notifier.view.id),
|
||||
view: notifier.view,
|
||||
shrinkWrap: shrinkWrap,
|
||||
initialRowId: initialRowId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -270,7 +292,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
value: bloc,
|
||||
child: Row(
|
||||
children: [
|
||||
DatabaseShareButton(key: ValueKey(view.id), view: view),
|
||||
ShareButton(key: ValueKey(view.id), view: view),
|
||||
const HSpace(10),
|
||||
ViewFavoriteButton(view: view),
|
||||
const HSpace(4),
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
+24
-7
@@ -1,3 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/summary_cell_bloc.dart';
|
||||
@@ -7,19 +10,18 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/desktop_row_detail_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_summary_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/dispatch/error.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
abstract class IEditableSummaryCellSkin {
|
||||
@@ -149,7 +151,22 @@ class SummaryCellAccessory extends StatelessWidget {
|
||||
rowId: rowId,
|
||||
fieldId: fieldId,
|
||||
),
|
||||
child: BlocBuilder<SummaryRowBloc, SummaryRowState>(
|
||||
child: BlocConsumer<SummaryRowBloc, SummaryRowState>(
|
||||
listenWhen: (previous, current) {
|
||||
return previous.error != current.error;
|
||||
},
|
||||
listener: (context, state) {
|
||||
if (state.error != null) {
|
||||
if (state.error!.isAIResponseLimitExceeded) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),
|
||||
);
|
||||
} else {
|
||||
showSnackBarMessage(context, state.error!.msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return const Row(
|
||||
children: [SummaryButton(), HSpace(6), CopyButton()],
|
||||
@@ -169,13 +186,13 @@ class SummaryButton extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SummaryRowBloc, SummaryRowState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.map(
|
||||
loading: (_) {
|
||||
return state.loadingState.when(
|
||||
loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
},
|
||||
finish: (_) {
|
||||
finish: () {
|
||||
return FlowyTooltip(
|
||||
message: LocaleKeys.tooltip_aiGenerate.tr(),
|
||||
child: Container(
|
||||
|
||||
+21
-4
@@ -1,3 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/translate_cell_bloc.dart';
|
||||
@@ -7,19 +10,18 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller_build
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_grid/desktop_grid_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/desktop_row_detail/destop_row_detail_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_grid/mobile_grid_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/mobile_row_detail/mobile_row_detail_translate_cell.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy_backend/dispatch/error.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
abstract class IEditableTranslateCellSkin {
|
||||
@@ -150,7 +152,22 @@ class TranslateCellAccessory extends StatelessWidget {
|
||||
rowId: rowId,
|
||||
fieldId: fieldId,
|
||||
),
|
||||
child: BlocBuilder<TranslateRowBloc, TranslateRowState>(
|
||||
child: BlocConsumer<TranslateRowBloc, TranslateRowState>(
|
||||
listenWhen: (previous, current) {
|
||||
return previous.error != current.error;
|
||||
},
|
||||
listener: (context, state) {
|
||||
if (state.error != null) {
|
||||
if (state.error!.isAIResponseLimitExceeded) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),
|
||||
);
|
||||
} else {
|
||||
showSnackBarMessage(context, state.error!.msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return const Row(
|
||||
children: [TranslateButton(), HSpace(6), CopyButton()],
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:percent_indicator/percent_indicator.dart';
|
||||
|
||||
import '../../application/cell/bloc/checklist_cell_bloc.dart';
|
||||
|
||||
class ChecklistProgressBar extends StatefulWidget {
|
||||
@@ -68,7 +68,7 @@ class _ChecklistProgressBarState extends State<ChecklistProgressBar> {
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: PlatformExtension.isDesktop ? 36 : 45,
|
||||
width: 45,
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: Text(
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DatabaseViewWidget extends StatefulWidget {
|
||||
const DatabaseViewWidget({
|
||||
@@ -55,6 +55,9 @@ class _DatabaseViewWidgetState extends State<DatabaseViewWidget> {
|
||||
builder: (_, __, ___) => viewPlugin.widgetBuilder.buildWidget(
|
||||
shrinkWrap: widget.shrinkWrap,
|
||||
context: PluginContext(),
|
||||
data: {
|
||||
kDatabasePluginWidgetBuilderHorizontalPadding: 40.0,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -104,6 +104,7 @@ class DatabaseDocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) {
|
||||
return BlocBuilder<DocumentAppearanceCubit, DocumentAppearance>(
|
||||
builder: (_, state) => DatabaseDocumentPage(
|
||||
|
||||
@@ -18,6 +18,7 @@ import 'package:appflowy/util/color_to_hex_string.dart';
|
||||
import 'package:appflowy/util/debounce.dart';
|
||||
import 'package:appflowy/util/throttle.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-document/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
@@ -37,6 +38,8 @@ import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'document_bloc.freezed.dart';
|
||||
|
||||
bool enableDocumentInternalLog = false;
|
||||
|
||||
class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
DocumentBloc({
|
||||
required this.documentId,
|
||||
@@ -212,6 +215,10 @@ class DocumentBloc extends Bloc<DocumentEvent, DocumentState> {
|
||||
}
|
||||
|
||||
Future<EditorState?> _initAppFlowyEditorState(DocumentDataPB data) async {
|
||||
if (enableDocumentInternalLog) {
|
||||
Log.info('document data: ${data.toProto3Json()}');
|
||||
}
|
||||
|
||||
final document = data.toDocument();
|
||||
if (document == null) {
|
||||
assert(false, 'document is null');
|
||||
|
||||
+8
@@ -180,6 +180,8 @@ extension NodeToBlock on Node {
|
||||
String? parentId,
|
||||
String? childrenId,
|
||||
Attributes? attributes,
|
||||
String? externalId,
|
||||
String? externalType,
|
||||
}) {
|
||||
assert(id.isNotEmpty);
|
||||
final block = BlockPB.create()
|
||||
@@ -192,6 +194,12 @@ extension NodeToBlock on Node {
|
||||
if (parentId != null && parentId.isNotEmpty) {
|
||||
block.parentId = parentId;
|
||||
}
|
||||
if (externalId != null && externalId.isNotEmpty) {
|
||||
block.externalId = externalId;
|
||||
}
|
||||
if (externalType != null && externalType.isNotEmpty) {
|
||||
block.externalType = externalType;
|
||||
}
|
||||
return block;
|
||||
}
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ class DocumentService {
|
||||
}) async {
|
||||
final workspace = await FolderEventReadCurrentWorkspace().send();
|
||||
return workspace.fold((l) async {
|
||||
final payload = UploadedFilePB(
|
||||
final payload = DownloadFilePB(
|
||||
url: url,
|
||||
);
|
||||
final result = await DocumentEventDownloadFile(payload).send();
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.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:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'document_share_bloc.freezed.dart';
|
||||
|
||||
class DocumentShareBloc extends Bloc<DocumentShareEvent, DocumentShareState> {
|
||||
DocumentShareBloc({
|
||||
required this.view,
|
||||
}) : super(const DocumentShareState.initial()) {
|
||||
on<DocumentShareEvent>((event, emit) async {
|
||||
await event.when(
|
||||
share: (type, path) async {
|
||||
if (DocumentShareType.unimplemented.contains(type)) {
|
||||
Log.error('DocumentShareType $type is not implemented');
|
||||
return;
|
||||
}
|
||||
|
||||
emit(const DocumentShareState.loading());
|
||||
|
||||
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),
|
||||
);
|
||||
});
|
||||
|
||||
emit(DocumentShareState.finish(result));
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
ExportDataPB _saveMarkdownToPath(String markdown, String path) {
|
||||
File(path).writeAsStringSync(markdown);
|
||||
return ExportDataPB()
|
||||
..data = markdown
|
||||
..exportType = ExportType.Markdown;
|
||||
}
|
||||
|
||||
ExportDataPB _saveHTMLToPath(String html, String path) {
|
||||
File(path).writeAsStringSync(html);
|
||||
return ExportDataPB()
|
||||
..data = html
|
||||
..exportType = ExportType.HTML;
|
||||
}
|
||||
}
|
||||
|
||||
enum DocumentShareType {
|
||||
markdown,
|
||||
html,
|
||||
text,
|
||||
link;
|
||||
|
||||
static List<DocumentShareType> get unimplemented => [text, link];
|
||||
|
||||
DocumentExportType get exportType {
|
||||
switch (this) {
|
||||
case DocumentShareType.markdown:
|
||||
return DocumentExportType.markdown;
|
||||
case DocumentShareType.html:
|
||||
return DocumentExportType.html;
|
||||
case DocumentShareType.text:
|
||||
return DocumentExportType.text;
|
||||
case DocumentShareType.link:
|
||||
throw UnsupportedError('DocumentShareType.link is not supported');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentShareEvent with _$DocumentShareEvent {
|
||||
const factory DocumentShareEvent.share(DocumentShareType type, String? path) =
|
||||
Share;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DocumentShareState with _$DocumentShareState {
|
||||
const factory DocumentShareState.initial() = _Initial;
|
||||
const factory DocumentShareState.loading() = _Loading;
|
||||
const factory DocumentShareState.finish(
|
||||
FlowyResult<ExportDataPB, FlowyError> successOrFail,
|
||||
) = _Finish;
|
||||
}
|
||||
+23
-13
@@ -1,6 +1,7 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@@ -21,6 +22,8 @@ import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:nanoid/nanoid.dart';
|
||||
|
||||
const _kExternalTextType = 'text';
|
||||
|
||||
/// Uses to adjust the data structure between the editor and the backend.
|
||||
///
|
||||
/// The editor uses a tree structure to represent the document, while the backend uses a flat structure.
|
||||
@@ -34,11 +37,9 @@ class TransactionAdapter {
|
||||
final DocumentService documentService;
|
||||
final String documentId;
|
||||
|
||||
final bool _enableDebug = false;
|
||||
|
||||
Future<void> apply(Transaction transaction, EditorState editorState) async {
|
||||
final stopwatch = Stopwatch()..start();
|
||||
if (_enableDebug) {
|
||||
if (enableDocumentInternalLog) {
|
||||
Log.debug('transaction => ${transaction.toJson()}');
|
||||
}
|
||||
final actions = transaction.operations
|
||||
@@ -60,8 +61,10 @@ class TransactionAdapter {
|
||||
textId: payload.textId,
|
||||
delta: payload.delta,
|
||||
);
|
||||
if (_enableDebug) {
|
||||
Log.debug('create external text: ${payload.delta}');
|
||||
if (enableDocumentInternalLog) {
|
||||
Log.debug(
|
||||
'[editor_transaction_adapter] create external text: ${payload.delta}',
|
||||
);
|
||||
}
|
||||
} else if (type == TextDeltaType.update) {
|
||||
await documentService.updateExternalText(
|
||||
@@ -69,8 +72,10 @@ class TransactionAdapter {
|
||||
textId: payload.textId,
|
||||
delta: payload.delta,
|
||||
);
|
||||
if (_enableDebug) {
|
||||
Log.debug('update external text: ${payload.delta}');
|
||||
if (enableDocumentInternalLog) {
|
||||
Log.debug(
|
||||
'[editor_transaction_adapter] update external text: ${payload.delta}',
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,9 +87,9 @@ class TransactionAdapter {
|
||||
);
|
||||
final elapsed = stopwatch.elapsedMilliseconds;
|
||||
stopwatch.stop();
|
||||
if (_enableDebug) {
|
||||
if (enableDocumentInternalLog) {
|
||||
Log.debug(
|
||||
'apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
|
||||
'[editor_transaction_adapter] apply transaction cost: total $elapsed ms, converter action $actionCostTime ms, apply action ${elapsed - actionCostTime} ms',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -136,8 +141,9 @@ extension on InsertOperation {
|
||||
// create the external text if the node contains the delta in its data.
|
||||
final delta = node.delta;
|
||||
TextDeltaPayloadPB? textDeltaPayloadPB;
|
||||
String? textId;
|
||||
if (delta != null) {
|
||||
final textId = nanoid(6);
|
||||
textId = nanoid(6);
|
||||
|
||||
textDeltaPayloadPB = TextDeltaPayloadPB(
|
||||
documentId: documentId,
|
||||
@@ -148,13 +154,17 @@ extension on InsertOperation {
|
||||
// sync the text id to the node
|
||||
node.externalValues = ExternalValues(
|
||||
externalId: textId,
|
||||
externalType: 'text',
|
||||
externalType: _kExternalTextType,
|
||||
);
|
||||
}
|
||||
|
||||
// remove the delta from the data when the incremental update is stable.
|
||||
final payload = BlockActionPayloadPB()
|
||||
..block = node.toBlock(childrenId: nanoid(6))
|
||||
..block = node.toBlock(
|
||||
childrenId: nanoid(6),
|
||||
externalId: textId,
|
||||
externalType: textId != null ? _kExternalTextType : null,
|
||||
)
|
||||
..parentId = parentId
|
||||
..prevId = prevId;
|
||||
|
||||
@@ -248,7 +258,7 @@ extension on UpdateOperation {
|
||||
|
||||
node.externalValues = ExternalValues(
|
||||
externalId: textId,
|
||||
externalType: 'text',
|
||||
externalType: _kExternalTextType,
|
||||
);
|
||||
|
||||
actions.add(
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export '../../shared/share/share_bloc.dart';
|
||||
export 'document_bloc.dart';
|
||||
export 'document_service.dart';
|
||||
export 'document_share_bloc.dart';
|
||||
@@ -5,7 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
@@ -109,6 +109,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
final deletedView = notifier.isDeleted.value;
|
||||
@@ -154,7 +155,7 @@ class DocumentPluginWidgetBuilder extends PluginWidgetBuilder
|
||||
const HSpace(16),
|
||||
]
|
||||
: [const HSpace(8)],
|
||||
DocumentShareButton(
|
||||
ShareButton(
|
||||
key: ValueKey('share_button_${view.id}'),
|
||||
view: view,
|
||||
),
|
||||
|
||||
+1
-5
@@ -13,9 +13,7 @@ import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class BuiltInPageWidget extends StatefulWidget {
|
||||
@@ -127,7 +125,6 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.information_s,
|
||||
),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
// setting
|
||||
const Space(7, 0),
|
||||
@@ -141,7 +138,6 @@ class _BuiltInPageWidgetState extends State<BuiltInPageWidget> {
|
||||
width: 24,
|
||||
height: 24,
|
||||
iconPadding: const EdgeInsets.all(3),
|
||||
iconColorOnHover: Theme.of(context).colorScheme.onSecondary,
|
||||
icon: const FlowySvg(
|
||||
FlowySvgs.settings_s,
|
||||
),
|
||||
|
||||
+16
-5
@@ -1,16 +1,17 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
CodeBlockCopyBuilder codeBlockCopyBuilder =
|
||||
(_, node) => _CopyButton(node: node);
|
||||
@@ -28,16 +29,26 @@ class _CopyButton extends StatelessWidget {
|
||||
message: LocaleKeys.document_codeBlock_copyTooltip.tr(),
|
||||
child: FlowyIconButton(
|
||||
onPressed: () async {
|
||||
final delta = node.delta;
|
||||
if (delta == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final document = Document.blank()
|
||||
..insert([0], [node.copyWith()])
|
||||
..toJson();
|
||||
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
plainText: node.delta?.toPlainText(),
|
||||
plainText: delta.toPlainText(),
|
||||
inAppJson: jsonEncode(document.toJson()),
|
||||
),
|
||||
);
|
||||
|
||||
if (context.mounted) {
|
||||
showSnackBarMessage(
|
||||
showToastNotification(
|
||||
context,
|
||||
LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
|
||||
message: LocaleKeys.document_codeBlock_codeCopiedSnackbar.tr(),
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
+2
-2
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/editor_state_paste_node_extension.dart';
|
||||
@@ -10,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_p
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
|
||||
@@ -54,6 +53,7 @@ CommandShortcutEventHandler _pasteCommandHandler = (editorState) {
|
||||
|
||||
// try to paste the content in order, if any of them is failed, then try the next one
|
||||
if (inAppJson != null && inAppJson.isNotEmpty) {
|
||||
debugPrint('paste in app json: $inAppJson');
|
||||
await editorState.deleteSelectionIfNeeded();
|
||||
if (await editorState.pasteInAppJson(inAppJson)) {
|
||||
return;
|
||||
|
||||
+13
-3
@@ -75,7 +75,9 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
child: BlocConsumer<DocumentImmersiveCoverBloc,
|
||||
DocumentImmersiveCoverState>(
|
||||
listener: (context, state) {
|
||||
textEditingController.text = state.name;
|
||||
if (textEditingController.text.isEmpty) {
|
||||
textEditingController.text = state.name;
|
||||
}
|
||||
},
|
||||
builder: (_, state) {
|
||||
final iconAndTitle = _buildIconAndTitle(context, state);
|
||||
@@ -159,8 +161,16 @@ class _DocumentImmersiveCoverState extends State<DocumentImmersiveCover> {
|
||||
state.cover.isNone || state.cover.isPresets ? null : Colors.white,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
onChanged: _rename,
|
||||
onSubmitted: _rename,
|
||||
onChanged: (name) => Debounce.debounce(
|
||||
'rename',
|
||||
const Duration(milliseconds: 300),
|
||||
() => _rename(name),
|
||||
),
|
||||
onSubmitted: (name) => Debounce.debounce(
|
||||
'rename',
|
||||
const Duration(milliseconds: 300),
|
||||
() => _rename(name),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+8
-1
@@ -6,6 +6,7 @@ import 'package:appflowy/shared/custom_image_cache_manager.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/file_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/application_data_storage.dart';
|
||||
import 'package:appflowy_backend/dispatch/error.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
@@ -63,6 +64,12 @@ Future<(String? path, String? errorMessage)> saveImageToCloudStorage(
|
||||
);
|
||||
return (s.url, null);
|
||||
},
|
||||
(e) => (null, e.msg),
|
||||
(err) {
|
||||
if (err.isStorageLimitExceeded) {
|
||||
return (null, LocaleKeys.sideBar_storageLimitDialogTitle.tr());
|
||||
} else {
|
||||
return (null, err.msg);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
+2
-1
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
@@ -7,7 +9,6 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class OpenAIImageWidget extends StatefulWidget {
|
||||
const OpenAIImageWidget({
|
||||
|
||||
+21
-5
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/plugins/document/application/document_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/aa_menu/_close_keyboard_or_menu_button.dart';
|
||||
@@ -9,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_too
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
@@ -284,6 +286,14 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
|
||||
if (canUpdateCachedKeyboardHeight) {
|
||||
cachedKeyboardHeight.value = height;
|
||||
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
// cache the keyboard height with the view padding in Android
|
||||
if (cachedKeyboardHeight.value != 0) {
|
||||
cachedKeyboardHeight.value +=
|
||||
MediaQuery.of(context).viewPadding.bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (height == 0) {
|
||||
@@ -385,13 +395,19 @@ class _MobileToolbarState extends State<_MobileToolbar>
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: showMenuNotifier,
|
||||
builder: (_, showingMenu, __) {
|
||||
var paddingHeight = height;
|
||||
if (Platform.isAndroid) {
|
||||
paddingHeight =
|
||||
height + MediaQuery.of(context).viewPadding.bottom;
|
||||
var keyboardHeight = height;
|
||||
if (defaultTargetPlatform == TargetPlatform.android) {
|
||||
if (!showingMenu) {
|
||||
// take the max value of the keyboard height and the view padding
|
||||
// to make sure the toolbar is above the keyboard
|
||||
keyboardHeight = max(
|
||||
keyboardHeight,
|
||||
MediaQuery.of(context).viewInsets.bottom,
|
||||
);
|
||||
}
|
||||
}
|
||||
return SizedBox(
|
||||
height: paddingHeight,
|
||||
height: keyboardHeight,
|
||||
child: (showingMenu && selectedMenuIndex != null)
|
||||
? widget.toolbarItems[selectedMenuIndex!].menuBuilder?.call(
|
||||
context,
|
||||
|
||||
+13
-1
@@ -1,14 +1,26 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'error.freezed.dart';
|
||||
part 'error.g.dart';
|
||||
|
||||
@freezed
|
||||
class AIError with _$AIError {
|
||||
const factory AIError({
|
||||
String? code,
|
||||
required String message,
|
||||
@Default(AIErrorCode.other) AIErrorCode code,
|
||||
}) = _AIError;
|
||||
|
||||
factory AIError.fromJson(Map<String, Object?> json) =>
|
||||
_$AIErrorFromJson(json);
|
||||
}
|
||||
|
||||
enum AIErrorCode {
|
||||
@JsonValue('AIResponseLimitExceeded')
|
||||
aiResponseLimitExceeded,
|
||||
@JsonValue('Other')
|
||||
other,
|
||||
}
|
||||
|
||||
extension AIErrorExtension on AIError {
|
||||
bool get isLimitExceeded => code == AIErrorCode.aiResponseLimitExceeded;
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
void showAILimitDialog(BuildContext context, String message) {
|
||||
showConfirmDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.sideBar_aiResponseLimitDialogTitle.tr(),
|
||||
description: message,
|
||||
);
|
||||
}
|
||||
+50
-41
@@ -1,6 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
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/error.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';
|
||||
@@ -9,14 +12,11 @@ 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/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:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'ai_limit_dialog.dart';
|
||||
|
||||
class AutoCompletionBlockKeys {
|
||||
const AutoCompletionBlockKeys._();
|
||||
|
||||
@@ -43,7 +43,7 @@ Node autoCompletionNode({
|
||||
SelectionMenuItem autoGeneratorMenuItem = SelectionMenuItem.node(
|
||||
getName: LocaleKeys.document_plugins_autoGeneratorMenuItemName.tr,
|
||||
iconData: Icons.generating_tokens,
|
||||
keywords: ['ai', 'openai' 'writer', 'autogenerator'],
|
||||
keywords: ['ai', 'openai', 'writer', 'ai writer', 'autogenerator'],
|
||||
nodeBuilder: (editorState, _) {
|
||||
final node = autoCompletionNode(start: editorState.selection!);
|
||||
return node;
|
||||
@@ -127,7 +127,6 @@ class _AutoCompletionBlockComponentState
|
||||
_unsubscribeSelectionGesture();
|
||||
controller.dispose();
|
||||
textFieldFocusNode.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -178,9 +177,7 @@ class _AutoCompletionBlockComponentState
|
||||
final transaction = editorState.transaction..deleteNode(widget.node);
|
||||
await editorState.apply(
|
||||
transaction,
|
||||
options: const ApplyOptions(
|
||||
recordUndo: false,
|
||||
),
|
||||
options: const ApplyOptions(recordUndo: false),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -225,11 +222,16 @@ class _AutoCompletionBlockComponentState
|
||||
onError: (error) async {
|
||||
barrierDialog?.dismiss();
|
||||
if (mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
error.message,
|
||||
showCancel: true,
|
||||
);
|
||||
if (error.isLimitExceeded) {
|
||||
showAILimitDialog(context, error.message);
|
||||
await _onDiscard();
|
||||
} else {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
error.message,
|
||||
showCancel: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -304,11 +306,15 @@ class _AutoCompletionBlockComponentState
|
||||
onEnd: () async {},
|
||||
onError: (error) async {
|
||||
if (mounted) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
error.message,
|
||||
showCancel: true,
|
||||
);
|
||||
if (error.isLimitExceeded) {
|
||||
showAILimitDialog(context, error.message);
|
||||
} else {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
error.message,
|
||||
showCancel: true,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -406,12 +412,10 @@ class _AutoCompletionBlockComponentState
|
||||
// show dialog
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return DiscardDialog(
|
||||
onConfirm: () => _onDiscard(),
|
||||
onCancel: () {},
|
||||
);
|
||||
},
|
||||
builder: (_) => DiscardDialog(
|
||||
onConfirm: _onDiscard,
|
||||
onCancel: () {},
|
||||
),
|
||||
);
|
||||
} else if (controller.text.isEmpty) {
|
||||
_onExit();
|
||||
@@ -434,9 +438,7 @@ class _AutoCompletionBlockComponentState
|
||||
}
|
||||
|
||||
class AutoCompletionHeader extends StatelessWidget {
|
||||
const AutoCompletionHeader({
|
||||
super.key,
|
||||
});
|
||||
const AutoCompletionHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -460,23 +462,27 @@ class AutoCompletionInputFooter extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_generate.tr(),
|
||||
FlowyTextButton.primary(
|
||||
text: LocaleKeys.button_generate.tr(),
|
||||
context: context,
|
||||
onPressed: onGenerate,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_cancel.tr(),
|
||||
FlowyTextButton.secondary(
|
||||
text: LocaleKeys.button_cancel.tr(),
|
||||
context: context,
|
||||
onPressed: onExit,
|
||||
),
|
||||
Expanded(
|
||||
Flexible(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: FlowyText.regular(
|
||||
LocaleKeys.document_plugins_warning.tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -501,18 +507,21 @@ class AutoCompletionFooter extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
children: [
|
||||
PrimaryTextButton(
|
||||
LocaleKeys.button_keep.tr(),
|
||||
FlowyTextButton.primary(
|
||||
context: context,
|
||||
text: LocaleKeys.button_keep.tr(),
|
||||
onPressed: onKeep,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
|
||||
FlowyTextButton.secondary(
|
||||
context: context,
|
||||
text: LocaleKeys.document_plugins_autoGeneratorRewrite.tr(),
|
||||
onPressed: onRewrite,
|
||||
),
|
||||
const Space(10, 0),
|
||||
SecondaryTextButton(
|
||||
LocaleKeys.button_discard.tr(),
|
||||
FlowyTextButton.secondary(
|
||||
context: context,
|
||||
text: LocaleKeys.button_discard.tr(),
|
||||
onPressed: onDiscard,
|
||||
),
|
||||
],
|
||||
|
||||
+12
-5
@@ -2,6 +2,7 @@ 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/widgets/discard_dialog.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/smart_edit_action.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
@@ -16,6 +17,8 @@ import 'package:flutter/material.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'ai_limit_dialog.dart';
|
||||
|
||||
class SmartEditBlockKeys {
|
||||
const SmartEditBlockKeys._();
|
||||
|
||||
@@ -426,11 +429,15 @@ class _SmartEditInputWidgetState extends State<SmartEditInputWidget> {
|
||||
});
|
||||
},
|
||||
onError: (error) async {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
error.message,
|
||||
showCancel: true,
|
||||
);
|
||||
if (error.isLimitExceeded) {
|
||||
showAILimitDialog(context, error.message);
|
||||
} else {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
error.message,
|
||||
showCancel: true,
|
||||
);
|
||||
}
|
||||
await _onExit();
|
||||
},
|
||||
);
|
||||
|
||||
+6
-12
@@ -1,6 +1,8 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
|
||||
@@ -23,7 +25,6 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/snap_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:image_picker/image_picker.dart';
|
||||
@@ -239,19 +240,15 @@ class PageStyleCoverImage extends StatelessWidget {
|
||||
return;
|
||||
}
|
||||
if (result == null) {
|
||||
showSnapBar(
|
||||
return showSnapBar(
|
||||
context,
|
||||
LocaleKeys.document_plugins_image_imageUploadFailed,
|
||||
LocaleKeys.document_plugins_image_imageUploadFailed.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
context.read<DocumentPageStyleBloc>().add(
|
||||
DocumentPageStyleEvent.updateCoverImage(
|
||||
PageStyleCover(
|
||||
type: type,
|
||||
value: result,
|
||||
),
|
||||
PageStyleCover(type: type, value: result),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -282,10 +279,7 @@ class PageStyleCoverImage extends StatelessWidget {
|
||||
},
|
||||
builder: (_) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxHeight: maxHeight,
|
||||
minHeight: 80,
|
||||
),
|
||||
constraints: BoxConstraints(maxHeight: maxHeight, minHeight: 80),
|
||||
child: BlocProvider.value(
|
||||
value: pageStyleBloc,
|
||||
child: Padding(
|
||||
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:markdown/markdown.dart' as md;
|
||||
|
||||
class MarkdownCodeBlockParser extends CustomMarkdownParser {
|
||||
const MarkdownCodeBlockParser();
|
||||
|
||||
@override
|
||||
List<Node> transform(
|
||||
md.Node element,
|
||||
List<CustomMarkdownParser> parsers, {
|
||||
MarkdownListType listType = MarkdownListType.unknown,
|
||||
int? startNumber,
|
||||
}) {
|
||||
if (element is! md.Element) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (element.tag != 'pre') {
|
||||
return [];
|
||||
}
|
||||
|
||||
final ec = element.children;
|
||||
if (ec == null || ec.isEmpty) {
|
||||
return [];
|
||||
}
|
||||
|
||||
final code = ec.first;
|
||||
if (code is! md.Element || code.tag != 'code') {
|
||||
return [];
|
||||
}
|
||||
|
||||
String? language;
|
||||
if (code.attributes.containsKey('class')) {
|
||||
final classes = code.attributes['class']!.split(' ');
|
||||
final languageClass = classes.firstWhere(
|
||||
(c) => c.startsWith('language-'),
|
||||
orElse: () => '',
|
||||
);
|
||||
language = languageClass.substring('language-'.length);
|
||||
}
|
||||
|
||||
return [
|
||||
codeBlockNode(
|
||||
language: language,
|
||||
delta: Delta()..insert(code.textContent.trimRight()),
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
+1
@@ -1,3 +1,4 @@
|
||||
export 'callout_node_parser.dart';
|
||||
export 'markdown_code_parser.dart';
|
||||
export 'math_equation_node_parser.dart';
|
||||
export 'toggle_list_node_parser.dart';
|
||||
@@ -1,207 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_share_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.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-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_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/widget/rounded_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class DocumentShareButton extends StatelessWidget {
|
||||
const DocumentShareButton({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
create: (context) => getIt<DocumentShareBloc>(param1: view),
|
||||
child: BlocListener<DocumentShareBloc, DocumentShareState>(
|
||||
listener: (context, state) {
|
||||
state.mapOrNull(
|
||||
finish: (state) {
|
||||
state.successOrFail.fold(
|
||||
(data) => _handleExportData(context, data),
|
||||
_handleExportError,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: BlocBuilder<DocumentShareBloc, DocumentShareState>(
|
||||
builder: (context, state) => SizedBox(
|
||||
height: 32.0,
|
||||
child: IntrinsicWidth(child: ShareActionList(view: view)),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleExportData(BuildContext context, ExportDataPB exportData) {
|
||||
switch (exportData.exportType) {
|
||||
case ExportType.Markdown:
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
break;
|
||||
case ExportType.Link:
|
||||
case ExportType.Text:
|
||||
break;
|
||||
case ExportType.HTML:
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleExportError(FlowyError error) {
|
||||
showMessageToast(error.msg);
|
||||
}
|
||||
}
|
||||
|
||||
class ShareActionList extends StatefulWidget {
|
||||
const ShareActionList({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
State<ShareActionList> createState() => ShareActionListState();
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
class ShareActionListState extends State<ShareActionList> {
|
||||
late String name;
|
||||
late final ViewListener viewListener = ViewListener(viewId: widget.view.id);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
listenOnViewUpdated();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
viewListener.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final docShareBloc = context.read<DocumentShareBloc>();
|
||||
return PopoverActionList<ShareActionWrapper>(
|
||||
direction: PopoverDirection.bottomWithCenterAligned,
|
||||
offset: const Offset(0, 8),
|
||||
actions: ShareAction.values
|
||||
.map((action) => ShareActionWrapper(action))
|
||||
.toList(),
|
||||
buildChild: (controller) => Listener(
|
||||
onPointerDown: (_) => controller.show(),
|
||||
child: RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
onPressed: () {},
|
||||
fontSize: 14.0,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
onSelected: (action, controller) async {
|
||||
switch (action.inner) {
|
||||
case ShareAction.markdown:
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
// encode the file name in case it contains special characters
|
||||
fileName: '${name.toFileName()}.md',
|
||||
);
|
||||
if (exportPath != null) {
|
||||
docShareBloc.add(
|
||||
DocumentShareEvent.share(
|
||||
DocumentShareType.markdown,
|
||||
exportPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ShareAction.html:
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${name.toFileName()}.html',
|
||||
);
|
||||
if (exportPath != null) {
|
||||
docShareBloc.add(
|
||||
DocumentShareEvent.share(
|
||||
DocumentShareType.html,
|
||||
exportPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case ShareAction.clipboard:
|
||||
final documentExporter = DocumentExporter(widget.view);
|
||||
final result =
|
||||
await documentExporter.export(DocumentExportType.markdown);
|
||||
result.fold(
|
||||
(markdown) => getIt<ClipboardService>()
|
||||
.setData(ClipboardServiceData(plainText: markdown)),
|
||||
(error) => showMessageToast(error.msg),
|
||||
);
|
||||
break;
|
||||
}
|
||||
controller.close();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void listenOnViewUpdated() {
|
||||
name = widget.view.name;
|
||||
viewListener.start(
|
||||
onViewUpdated: (view) {
|
||||
name = view.name;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ShareAction {
|
||||
markdown,
|
||||
html,
|
||||
clipboard,
|
||||
}
|
||||
|
||||
class ShareActionWrapper extends ActionCell {
|
||||
ShareActionWrapper(this.inner);
|
||||
|
||||
final ShareAction inner;
|
||||
|
||||
Widget? icon(Color iconColor) => null;
|
||||
|
||||
@override
|
||||
String get name {
|
||||
switch (inner) {
|
||||
case ShareAction.markdown:
|
||||
return LocaleKeys.shareAction_markdown.tr();
|
||||
case ShareAction.html:
|
||||
return LocaleKeys.shareAction_html.tr();
|
||||
case ShareAction.clipboard:
|
||||
return LocaleKeys.shareAction_clipboard.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
+4
-5
@@ -8,6 +8,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/me
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_menu.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/inline_actions_result.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/service_handler.dart';
|
||||
import 'package:appflowy/shared/list_extension.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
@@ -64,11 +65,9 @@ class InlinePageReferenceService extends InlineActionsDelegate {
|
||||
|
||||
_recentViewsInitialized = true;
|
||||
|
||||
final views = (await _recentService.recentViews())
|
||||
.reversed
|
||||
.map((e) => e.item)
|
||||
.toSet()
|
||||
.toList();
|
||||
final sectionViews = await _recentService.recentViews();
|
||||
final views =
|
||||
sectionViews.unique((e) => e.item.id).map((e) => e.item).toList();
|
||||
|
||||
// Filter by viewLayout
|
||||
views.retainWhere(
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_menu.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/rounded_button.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ShareMenuButton extends StatelessWidget {
|
||||
const ShareMenuButton({
|
||||
super.key,
|
||||
required this.tabs,
|
||||
});
|
||||
|
||||
final List<ShareMenuTab> tabs;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final shareBloc = context.read<ShareBloc>();
|
||||
final databaseBloc = context.read<DatabaseTabBarBloc?>();
|
||||
return SizedBox(
|
||||
height: 32.0,
|
||||
child: IntrinsicWidth(
|
||||
child: AppFlowyPopover(
|
||||
direction: PopoverDirection.bottomWithRightAligned,
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: 422,
|
||||
),
|
||||
offset: const Offset(0, 8),
|
||||
popupBuilder: (context) => MultiBlocProvider(
|
||||
providers: [
|
||||
if (databaseBloc != null)
|
||||
BlocProvider.value(
|
||||
value: databaseBloc,
|
||||
),
|
||||
BlocProvider.value(value: shareBloc),
|
||||
],
|
||||
child: ShareMenu(
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
child: const _ShareButton(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ShareButton extends StatelessWidget {
|
||||
const _ShareButton();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoundedTextButton(
|
||||
title: LocaleKeys.shareAction_buttonText.tr(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14.0),
|
||||
fontSize: 14.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
borderRadius: const BorderRadius.all(
|
||||
Radius.circular(10.0),
|
||||
),
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/file_picker/file_picker_service.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ExportTab extends StatelessWidget {
|
||||
const ExportTab({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final view = context.read<ShareBloc>().view;
|
||||
|
||||
if (view.layout == ViewLayoutPB.Document) {
|
||||
return _buildDocumentExportTab(context);
|
||||
}
|
||||
|
||||
return _buildDatabaseExportTab(context);
|
||||
}
|
||||
|
||||
Widget _buildDocumentExportTab(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const VSpace(10),
|
||||
_ExportButton(
|
||||
title: LocaleKeys.shareAction_html.tr(),
|
||||
svg: FlowySvgs.export_html_s,
|
||||
onTap: () => _exportHTML(context),
|
||||
),
|
||||
const VSpace(10),
|
||||
_ExportButton(
|
||||
title: LocaleKeys.shareAction_markdown.tr(),
|
||||
svg: FlowySvgs.export_markdown_s,
|
||||
onTap: () => _exportMarkdown(context),
|
||||
),
|
||||
const VSpace(10),
|
||||
_ExportButton(
|
||||
title: LocaleKeys.shareAction_clipboard.tr(),
|
||||
svg: FlowySvgs.duplicate_s,
|
||||
onTap: () => _exportToClipboard(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDatabaseExportTab(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
const VSpace(10),
|
||||
_ExportButton(
|
||||
title: LocaleKeys.shareAction_csv.tr(),
|
||||
svg: FlowySvgs.database_layout_m,
|
||||
onTap: () => _exportCSV(context),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _exportHTML(BuildContext context) async {
|
||||
final viewName = context.read<ShareBloc>().state.viewName;
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${viewName.toFileName()}.html',
|
||||
);
|
||||
if (context.mounted && exportPath != null) {
|
||||
context.read<ShareBloc>().add(
|
||||
ShareEvent.share(
|
||||
ShareType.html,
|
||||
exportPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportMarkdown(BuildContext context) async {
|
||||
final viewName = context.read<ShareBloc>().state.viewName;
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${viewName.toFileName()}.md',
|
||||
);
|
||||
if (context.mounted && exportPath != null) {
|
||||
context.read<ShareBloc>().add(
|
||||
ShareEvent.share(
|
||||
ShareType.markdown,
|
||||
exportPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportCSV(BuildContext context) async {
|
||||
final viewName = context.read<ShareBloc>().state.viewName;
|
||||
final exportPath = await getIt<FilePickerService>().saveFile(
|
||||
dialogTitle: '',
|
||||
fileName: '${viewName.toFileName()}.csv',
|
||||
);
|
||||
if (context.mounted && exportPath != null) {
|
||||
context.read<ShareBloc>().add(
|
||||
ShareEvent.share(
|
||||
ShareType.csv,
|
||||
exportPath,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _exportToClipboard(BuildContext context) async {
|
||||
final documentExporter = DocumentExporter(context.read<ShareBloc>().view);
|
||||
final result = await documentExporter.export(DocumentExportType.markdown);
|
||||
result.fold(
|
||||
(markdown) {
|
||||
getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(plainText: markdown),
|
||||
);
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copiedNotification.tr(),
|
||||
);
|
||||
},
|
||||
(error) => showToastNotification(context, message: error.msg),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ExportButton extends StatelessWidget {
|
||||
const _ExportButton({
|
||||
required this.title,
|
||||
required this.svg,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final FlowySvgData svg;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = Theme.of(context).isLightMode
|
||||
? const Color(0x1E14171B)
|
||||
: Colors.white.withOpacity(0.1);
|
||||
final radius = BorderRadius.circular(10.0);
|
||||
return FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
|
||||
iconPadding: 12,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: color,
|
||||
),
|
||||
borderRadius: radius,
|
||||
),
|
||||
radius: radius,
|
||||
text: FlowyText(title),
|
||||
leftIcon: FlowySvg(svg),
|
||||
onTap: onTap,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ShareMenuColors {
|
||||
static Color borderColor(BuildContext context) {
|
||||
final borderColor = Theme.of(context).isLightMode
|
||||
? const Color(0x1E14171B)
|
||||
: Colors.white.withOpacity(0.1);
|
||||
return borderColor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
String replaceInvalidChars(String input) {
|
||||
final RegExp invalidCharsRegex = RegExp('[^a-zA-Z0-9-]');
|
||||
return input.replaceAll(invalidCharsRegex, '-');
|
||||
}
|
||||
|
||||
Future<String> generateNameSpace() async {
|
||||
return '';
|
||||
}
|
||||
|
||||
// The backend limits the publish name to a maximum of 120 characters.
|
||||
// If the combined length of the ID and the name exceeds 120 characters,
|
||||
// we will truncate the name to ensure the final result is within the limit.
|
||||
// The name should only contain alphanumeric characters and hyphens.
|
||||
Future<String> generatePublishName(String id, String name) async {
|
||||
if (name.length >= 120 - id.length) {
|
||||
name = name.substring(0, 120 - id.length);
|
||||
}
|
||||
return replaceInvalidChars('$name-$id');
|
||||
}
|
||||
@@ -0,0 +1,559 @@
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/plugins/shared/share/publish_color_extension.dart';
|
||||
import 'package:appflowy/plugins/shared/share/publish_name_generator.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.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<ShareBloc, ShareState>(
|
||||
listener: (context, state) {
|
||||
_showToast(context, state);
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state.isPublished) {
|
||||
return _PublishedWidget(
|
||||
url: state.url,
|
||||
onVisitSite: (url) => afLaunchUrlString(url),
|
||||
onUnPublish: () {
|
||||
context.read<ShareBloc>().add(const ShareEvent.unPublish());
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return _PublishWidget(
|
||||
onPublish: (selectedViews) async {
|
||||
final id = context.read<ShareBloc>().view.id;
|
||||
final publishName = await generatePublishName(
|
||||
id,
|
||||
state.viewName,
|
||||
);
|
||||
|
||||
if (selectedViews.isNotEmpty) {
|
||||
Log.info(
|
||||
'Publishing views: ${selectedViews.map((e) => e.name)}',
|
||||
);
|
||||
}
|
||||
|
||||
if (context.mounted) {
|
||||
context.read<ShareBloc>().add(
|
||||
ShareEvent.publish(
|
||||
'',
|
||||
publishName,
|
||||
selectedViews.map((e) => e.id).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showToast(BuildContext context, ShareState state) {
|
||||
if (state.publishResult != null) {
|
||||
state.publishResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_publishSuccessfully.tr(),
|
||||
),
|
||||
(error) => showToastNotification(
|
||||
context,
|
||||
message: '${LocaleKeys.publish_publishFailed.tr()}: ${error.code}',
|
||||
),
|
||||
);
|
||||
} else if (state.unpublishResult != null) {
|
||||
state.unpublishResult!.fold(
|
||||
(value) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishSuccessfully.tr(),
|
||||
),
|
||||
(error) => showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_unpublishFailed.tr(),
|
||||
description: error.msg,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishedWidget extends StatefulWidget {
|
||||
const _PublishedWidget({
|
||||
required this.url,
|
||||
required this.onVisitSite,
|
||||
required this.onUnPublish,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final void Function(String url) onVisitSite;
|
||||
final VoidCallback onUnPublish;
|
||||
|
||||
@override
|
||||
State<_PublishedWidget> createState() => _PublishedWidgetState();
|
||||
}
|
||||
|
||||
class _PublishedWidgetState extends State<_PublishedWidget> {
|
||||
final controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
controller.text = widget.url;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(16),
|
||||
const _PublishTabHeader(),
|
||||
const VSpace(16),
|
||||
_PublishUrl(
|
||||
controller: controller,
|
||||
onCopy: (url) {
|
||||
getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(plainText: url),
|
||||
);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.grid_url_copy.tr(),
|
||||
);
|
||||
},
|
||||
onSubmitted: (url) {},
|
||||
),
|
||||
const VSpace(16),
|
||||
Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_buildUnpublishButton(),
|
||||
const Spacer(),
|
||||
_buildVisitSiteButton(),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildUnpublishButton() {
|
||||
return SizedBox(
|
||||
width: 184,
|
||||
height: 36,
|
||||
child: FlowyButton(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
border: Border.all(color: ShareMenuColors.borderColor(context)),
|
||||
),
|
||||
radius: BorderRadius.circular(10),
|
||||
text: FlowyText.regular(
|
||||
LocaleKeys.shareAction_unPublish.tr(),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
onTap: widget.onUnPublish,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildVisitSiteButton() {
|
||||
return RoundedTextButton(
|
||||
width: 184,
|
||||
height: 36,
|
||||
onPressed: () => widget.onVisitSite(controller.text),
|
||||
title: LocaleKeys.shareAction_visitSite.tr(),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(10)),
|
||||
fillColor: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishWidget extends StatefulWidget {
|
||||
const _PublishWidget({
|
||||
required this.onPublish,
|
||||
});
|
||||
|
||||
final void Function(List<ViewPB> selectedViews) onPublish;
|
||||
|
||||
@override
|
||||
State<_PublishWidget> createState() => _PublishWidgetState();
|
||||
}
|
||||
|
||||
class _PublishWidgetState extends State<_PublishWidget> {
|
||||
List<ViewPB> _selectedViews = [];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(16),
|
||||
const _PublishTabHeader(),
|
||||
const VSpace(16),
|
||||
// if current view is a database, show the database selector
|
||||
if (context.read<ShareBloc>().view.layout.isDatabaseView) ...[
|
||||
_PublishDatabaseSelector(
|
||||
view: context.read<ShareBloc>().view,
|
||||
onSelected: (selectedDatabases) {
|
||||
_selectedViews = selectedDatabases;
|
||||
},
|
||||
),
|
||||
const VSpace(16),
|
||||
],
|
||||
_PublishButton(
|
||||
onPublish: () {
|
||||
if (context.read<ShareBloc>().view.layout.isDatabaseView) {
|
||||
// check if any database is selected
|
||||
if (_selectedViews.isEmpty) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.publish_noDatabaseSelected.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
widget.onPublish(_selectedViews);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishButton extends StatelessWidget {
|
||||
const _PublishButton({
|
||||
required this.onPublish,
|
||||
});
|
||||
|
||||
final VoidCallback onPublish;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RoundedTextButton(
|
||||
height: 36,
|
||||
title: LocaleKeys.shareAction_publish.tr(),
|
||||
padding: const EdgeInsets.symmetric(vertical: 9.0),
|
||||
fontSize: 14.0,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: onPublish,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishTabHeader extends StatelessWidget {
|
||||
const _PublishTabHeader();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
const FlowySvg(FlowySvgs.share_publish_s),
|
||||
const HSpace(6),
|
||||
FlowyText(LocaleKeys.shareAction_publishToTheWeb.tr()),
|
||||
],
|
||||
),
|
||||
const VSpace(4),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.shareAction_publishToTheWebHint.tr(),
|
||||
fontSize: 12,
|
||||
maxLines: 3,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PublishUrl extends StatelessWidget {
|
||||
const _PublishUrl({
|
||||
required this.controller,
|
||||
required this.onCopy,
|
||||
required this.onSubmitted,
|
||||
});
|
||||
|
||||
final TextEditingController controller;
|
||||
final void Function(String url) onCopy;
|
||||
final void Function(String url) onSubmitted;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: 36,
|
||||
child: FlowyTextField(
|
||||
readOnly: true,
|
||||
autoFocus: false,
|
||||
controller: controller,
|
||||
enableBorderColor: ShareMenuColors.borderColor(context),
|
||||
suffixIcon: _buildCopyLinkIcon(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCopyLinkIcon(BuildContext context) {
|
||||
return FlowyHover(
|
||||
child: GestureDetector(
|
||||
onTap: () => onCopy(controller.text),
|
||||
child: Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
alignment: Alignment.center,
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(left: BorderSide(color: Color(0x141F2329))),
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.m_toolbar_link_m,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// used to select which database view should be published
|
||||
class _PublishDatabaseSelector extends StatefulWidget {
|
||||
const _PublishDatabaseSelector({
|
||||
required this.view,
|
||||
required this.onSelected,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final void Function(List<ViewPB> selectedDatabases) onSelected;
|
||||
|
||||
@override
|
||||
State<_PublishDatabaseSelector> createState() =>
|
||||
_PublishDatabaseSelectorState();
|
||||
}
|
||||
|
||||
class _PublishDatabaseSelectorState extends State<_PublishDatabaseSelector> {
|
||||
final PropertyValueNotifier<List<(ViewPB, bool)>> _databaseStatus =
|
||||
PropertyValueNotifier<List<(ViewPB, bool)>>([]);
|
||||
late final _borderColor = Theme.of(context).hintColor.withOpacity(0.3);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_databaseStatus.addListener(() {
|
||||
final selectedDatabases =
|
||||
_databaseStatus.value.where((e) => e.$2).map((e) => e.$1).toList();
|
||||
widget.onSelected(selectedDatabases);
|
||||
});
|
||||
|
||||
_databaseStatus.value = context
|
||||
.read<DatabaseTabBarBloc>()
|
||||
.state
|
||||
.tabBars
|
||||
.map((e) => (e.view, true))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_databaseStatus.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<DatabaseTabBarBloc, DatabaseTabBarState>(
|
||||
builder: (context, state) {
|
||||
return Container(
|
||||
clipBehavior: Clip.antiAlias,
|
||||
decoration: ShapeDecoration(
|
||||
shape: RoundedRectangleBorder(
|
||||
side: BorderSide(color: _borderColor),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const VSpace(10),
|
||||
_buildSelectedDatabaseCount(context),
|
||||
const VSpace(10),
|
||||
_buildDivider(context),
|
||||
const VSpace(10),
|
||||
...state.tabBars.map(
|
||||
(e) => _buildDatabaseSelector(context, e),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDivider(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Divider(
|
||||
color: _borderColor,
|
||||
thickness: 1,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelectedDatabaseCount(BuildContext context) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _databaseStatus,
|
||||
builder: (context, selectedDatabases, child) {
|
||||
final count = selectedDatabases.where((e) => e.$2).length;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: FlowyText(
|
||||
LocaleKeys.publish_database.plural(count).tr(),
|
||||
color: Theme.of(context).hintColor,
|
||||
fontSize: 13,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDatabaseSelector(BuildContext context, DatabaseTabBar tabBar) {
|
||||
final isPrimaryDatabase = tabBar.view.id == widget.view.id;
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _databaseStatus,
|
||||
builder: (context, selectedDatabases, child) {
|
||||
final isSelected = selectedDatabases.any(
|
||||
(e) => e.$1.id == tabBar.view.id && e.$2,
|
||||
);
|
||||
return _DatabaseSelectorItem(
|
||||
tabBar: tabBar,
|
||||
isSelected: isSelected,
|
||||
isPrimaryDatabase: isPrimaryDatabase,
|
||||
onTap: () {
|
||||
// unable to deselect the primary database
|
||||
if (isPrimaryDatabase) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
LocaleKeys.publish_unableToDeselectPrimaryDatabase.tr(),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// toggle the selection status
|
||||
_databaseStatus.value = _databaseStatus.value
|
||||
.map(
|
||||
(e) =>
|
||||
e.$1.id == tabBar.view.id ? (e.$1, !e.$2) : (e.$1, e.$2),
|
||||
)
|
||||
.toList();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DatabaseSelectorItem extends StatelessWidget {
|
||||
const _DatabaseSelectorItem({
|
||||
required this.tabBar,
|
||||
required this.isSelected,
|
||||
required this.onTap,
|
||||
required this.isPrimaryDatabase,
|
||||
});
|
||||
|
||||
final DatabaseTabBar tabBar;
|
||||
final bool isSelected;
|
||||
final VoidCallback onTap;
|
||||
final bool isPrimaryDatabase;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget child = _buildItem(context);
|
||||
|
||||
if (!isPrimaryDatabase) {
|
||||
child = FlowyHover(
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onTap,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
child = FlowyTooltip(
|
||||
message: LocaleKeys.publish_mustSelectPrimaryDatabase.tr(),
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.forbidden,
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(BuildContext context) {
|
||||
final svg = isPrimaryDatabase
|
||||
? FlowySvgs.unable_select_s
|
||||
: isSelected
|
||||
? FlowySvgs.check_filled_s
|
||||
: FlowySvgs.uncheck_s;
|
||||
final blendMode = isPrimaryDatabase ? BlendMode.srcIn : null;
|
||||
return Container(
|
||||
height: 30,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
svg,
|
||||
blendMode: blendMode,
|
||||
size: const Size.square(18),
|
||||
),
|
||||
const HSpace(9.0),
|
||||
FlowySvg(
|
||||
tabBar.view.layout.icon,
|
||||
size: const Size.square(16),
|
||||
),
|
||||
const HSpace(6.0),
|
||||
FlowyText.regular(
|
||||
tabBar.view.name,
|
||||
fontSize: 14,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,272 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/user/application/user_service.dart';
|
||||
import 'package:appflowy/workspace/application/export/document_exporter.dart';
|
||||
import 'package:appflowy/workspace/application/settings/share/export_service.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'share_bloc.freezed.dart';
|
||||
|
||||
const _url = 'https://appflowy.com';
|
||||
|
||||
class ShareBloc extends Bloc<ShareEvent, ShareState> {
|
||||
ShareBloc({
|
||||
required this.view,
|
||||
}) : super(ShareState.initial()) {
|
||||
on<ShareEvent>((event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {
|
||||
viewListener = ViewListener(viewId: view.id)
|
||||
..start(
|
||||
onViewUpdated: (value) {
|
||||
add(ShareEvent.updateViewName(value.name));
|
||||
},
|
||||
onViewMoveToTrash: (p0) {
|
||||
add(const ShareEvent.setPublishStatus(false));
|
||||
},
|
||||
);
|
||||
|
||||
add(const ShareEvent.updatePublishStatus());
|
||||
},
|
||||
share: (type, path) async {
|
||||
if (ShareType.unimplemented.contains(type)) {
|
||||
Log.error('DocumentShareType $type is not implemented');
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(isLoading: true));
|
||||
|
||||
final result = await _export(type, path);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isLoading: false,
|
||||
exportResult: result,
|
||||
),
|
||||
);
|
||||
},
|
||||
publish: (nameSpace, publishName, selectedViewIds) async {
|
||||
// set space name
|
||||
try {
|
||||
final result =
|
||||
await ViewBackendService.getPublishNameSpace().getOrThrow();
|
||||
|
||||
await ViewBackendService.publish(
|
||||
view,
|
||||
name: publishName,
|
||||
selectedViewIds: selectedViewIds,
|
||||
).getOrThrow();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: true,
|
||||
publishResult: FlowySuccess(null),
|
||||
unpublishResult: null,
|
||||
url: '$_url/${result.namespace}/$publishName',
|
||||
),
|
||||
);
|
||||
|
||||
Log.info('publish success: ${result.namespace}/$publishName');
|
||||
} catch (e) {
|
||||
Log.error('publish error: $e');
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: false,
|
||||
publishResult: FlowyResult.failure(
|
||||
FlowyError(msg: 'publish error: $e'),
|
||||
),
|
||||
unpublishResult: null,
|
||||
url: '',
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
unPublish: () async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
publishResult: null,
|
||||
unpublishResult: null,
|
||||
),
|
||||
);
|
||||
|
||||
final result = await ViewBackendService.unpublish(view);
|
||||
final isPublished = !result.isSuccess;
|
||||
result.onFailure((f) {
|
||||
Log.error('unpublish error: $f');
|
||||
});
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: isPublished,
|
||||
publishResult: null,
|
||||
unpublishResult: result,
|
||||
url: result.fold((_) => '', (_) => state.url),
|
||||
),
|
||||
);
|
||||
},
|
||||
updateViewName: (viewName) async {
|
||||
emit(state.copyWith(viewName: viewName));
|
||||
},
|
||||
setPublishStatus: (isPublished) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: isPublished,
|
||||
url: isPublished ? state.url : '',
|
||||
),
|
||||
);
|
||||
},
|
||||
updatePublishStatus: () async {
|
||||
final publishInfo = await ViewBackendService.getPublishInfo(view);
|
||||
final enablePublish =
|
||||
await UserBackendService.getCurrentUserProfile().fold(
|
||||
(v) => v.authenticator == AuthenticatorPB.AppFlowyCloud,
|
||||
(p) => false,
|
||||
);
|
||||
publishInfo.fold((s) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: true,
|
||||
url: '$_url/${s.namespace}/${s.publishName}',
|
||||
viewName: view.name,
|
||||
enablePublish: enablePublish,
|
||||
),
|
||||
);
|
||||
}, (f) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isPublished: false,
|
||||
url: '',
|
||||
viewName: view.name,
|
||||
enablePublish: enablePublish,
|
||||
),
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
final ViewPB view;
|
||||
late final ViewListener viewListener;
|
||||
|
||||
late final documentExporter = DocumentExporter(view);
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await viewListener.stop();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
Future<FlowyResult<ShareType, FlowyError>> _export(
|
||||
ShareType type,
|
||||
String? path,
|
||||
) async {
|
||||
final FlowyResult<String, FlowyError> result;
|
||||
if (type == ShareType.csv) {
|
||||
final exportResult = await BackendExportService.exportDatabaseAsCSV(
|
||||
view.id,
|
||||
);
|
||||
result = exportResult.fold(
|
||||
(s) => FlowyResult.success(s.data),
|
||||
(f) => FlowyResult.failure(f),
|
||||
);
|
||||
} else {
|
||||
result = await documentExporter.export(type.documentExportType);
|
||||
}
|
||||
return result.fold(
|
||||
(s) {
|
||||
if (path != null) {
|
||||
switch (type) {
|
||||
case ShareType.markdown:
|
||||
case ShareType.html:
|
||||
case ShareType.csv:
|
||||
File(path).writeAsStringSync(s);
|
||||
return FlowyResult.success(type);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
return FlowyResult.failure(FlowyError());
|
||||
},
|
||||
(f) => FlowyResult.failure(f),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum ShareType {
|
||||
// available in document
|
||||
markdown,
|
||||
html,
|
||||
text,
|
||||
link,
|
||||
|
||||
// only available in database
|
||||
csv;
|
||||
|
||||
static List<ShareType> get unimplemented => [link];
|
||||
|
||||
DocumentExportType get documentExportType {
|
||||
switch (this) {
|
||||
case ShareType.markdown:
|
||||
return DocumentExportType.markdown;
|
||||
case ShareType.html:
|
||||
return DocumentExportType.html;
|
||||
case ShareType.text:
|
||||
return DocumentExportType.text;
|
||||
case ShareType.csv:
|
||||
throw UnsupportedError('DocumentShareType.csv is not supported');
|
||||
case ShareType.link:
|
||||
throw UnsupportedError('DocumentShareType.link is not supported');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ShareEvent with _$ShareEvent {
|
||||
const factory ShareEvent.initial() = _Initial;
|
||||
const factory ShareEvent.share(
|
||||
ShareType type,
|
||||
String? path,
|
||||
) = _Share;
|
||||
const factory ShareEvent.publish(
|
||||
String nameSpace,
|
||||
String pageId,
|
||||
List<String> selectedViewIds,
|
||||
) = _Publish;
|
||||
const factory ShareEvent.unPublish() = _UnPublish;
|
||||
const factory ShareEvent.updateViewName(String name) = _UpdateViewName;
|
||||
const factory ShareEvent.updatePublishStatus() = _UpdatePublishStatus;
|
||||
const factory ShareEvent.setPublishStatus(bool isPublished) =
|
||||
_SetPublishStatus;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ShareState with _$ShareState {
|
||||
const factory ShareState({
|
||||
required bool isPublished,
|
||||
required bool isLoading,
|
||||
required String url,
|
||||
required String viewName,
|
||||
required bool enablePublish,
|
||||
FlowyResult<ShareType, FlowyError>? exportResult,
|
||||
FlowyResult<void, FlowyError>? publishResult,
|
||||
FlowyResult<void, FlowyError>? unpublishResult,
|
||||
}) = _ShareState;
|
||||
|
||||
factory ShareState.initial() => const ShareState(
|
||||
isLoading: false,
|
||||
isPublished: false,
|
||||
enablePublish: true,
|
||||
url: '',
|
||||
viewName: '',
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.dart';
|
||||
import 'package:appflowy/plugins/shared/share/_shared.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_menu.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class ShareButton extends StatelessWidget {
|
||||
const ShareButton({
|
||||
super.key,
|
||||
required this.view,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (context) =>
|
||||
getIt<ShareBloc>(param1: view)..add(const ShareEvent.initial()),
|
||||
),
|
||||
if (view.layout.isDatabaseView)
|
||||
BlocProvider(
|
||||
create: (context) => DatabaseTabBarBloc(view: view)
|
||||
..add(const DatabaseTabBarEvent.initial()),
|
||||
),
|
||||
],
|
||||
child: BlocListener<ShareBloc, ShareState>(
|
||||
listener: (context, state) {
|
||||
if (!state.isLoading && state.exportResult != null) {
|
||||
state.exportResult!.fold(
|
||||
(data) => _handleExportSuccess(context, data),
|
||||
(error) => _handleExportError(context, error),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: BlocBuilder<ShareBloc, ShareState>(
|
||||
builder: (context, state) {
|
||||
final tabs = [
|
||||
if (state.enablePublish) ShareMenuTab.publish,
|
||||
ShareMenuTab.exportAs,
|
||||
];
|
||||
|
||||
return ShareMenuButton(tabs: tabs);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleExportSuccess(BuildContext context, ShareType shareType) {
|
||||
switch (shareType) {
|
||||
case ShareType.markdown:
|
||||
case ShareType.html:
|
||||
case ShareType.csv:
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.settings_files_exportFileSuccess.tr(),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void _handleExportError(BuildContext context, FlowyError error) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message:
|
||||
'${LocaleKeys.settings_files_exportFileFail.tr()}: ${error.code}',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/_round_underline_tab_indicator.dart';
|
||||
import 'package:appflowy/plugins/shared/share/export_tab.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'publish_tab.dart';
|
||||
|
||||
enum ShareMenuTab {
|
||||
share,
|
||||
publish,
|
||||
exportAs;
|
||||
|
||||
String get i18n {
|
||||
switch (this) {
|
||||
case ShareMenuTab.share:
|
||||
return LocaleKeys.shareAction_shareTab.tr();
|
||||
case ShareMenuTab.publish:
|
||||
return LocaleKeys.shareAction_publishTab.tr();
|
||||
case ShareMenuTab.exportAs:
|
||||
return LocaleKeys.shareAction_exportAsTab.tr();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ShareMenu extends StatefulWidget {
|
||||
const ShareMenu({
|
||||
super.key,
|
||||
required this.tabs,
|
||||
});
|
||||
|
||||
final List<ShareMenuTab> tabs;
|
||||
|
||||
@override
|
||||
State<ShareMenu> createState() => _ShareMenuState();
|
||||
}
|
||||
|
||||
class _ShareMenuState extends State<ShareMenu>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late ShareMenuTab selectedTab = widget.tabs.first;
|
||||
late final tabController = TabController(
|
||||
length: widget.tabs.length,
|
||||
vsync: this,
|
||||
initialIndex: widget.tabs.indexOf(selectedTab),
|
||||
);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.tabs.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const VSpace(10),
|
||||
Container(
|
||||
alignment: Alignment.centerLeft,
|
||||
height: 30,
|
||||
child: _buildTabBar(context),
|
||||
),
|
||||
Divider(
|
||||
color: Theme.of(context).dividerColor,
|
||||
height: 1,
|
||||
thickness: 1,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 14.0),
|
||||
child: _buildTab(context),
|
||||
),
|
||||
const VSpace(20),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
tabController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildTabBar(BuildContext context) {
|
||||
final children = [
|
||||
for (final tab in widget.tabs)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
child: _Segment(
|
||||
tab: tab,
|
||||
isSelected: selectedTab == tab,
|
||||
),
|
||||
),
|
||||
];
|
||||
return TabBar(
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicator: RoundUnderlineTabIndicator(
|
||||
width: 68.0,
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
width: 3,
|
||||
),
|
||||
insets: const EdgeInsets.only(bottom: -2),
|
||||
),
|
||||
isScrollable: true,
|
||||
controller: tabController,
|
||||
tabs: children,
|
||||
onTap: (index) {
|
||||
setState(() {
|
||||
selectedTab = widget.tabs[index];
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTab(BuildContext context) {
|
||||
switch (selectedTab) {
|
||||
case ShareMenuTab.publish:
|
||||
return const PublishTab();
|
||||
case ShareMenuTab.exportAs:
|
||||
return const ExportTab();
|
||||
default:
|
||||
return const Center(
|
||||
child: FlowyText('🏡 under construction'),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _Segment extends StatefulWidget {
|
||||
const _Segment({
|
||||
required this.tab,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final bool isSelected;
|
||||
final ShareMenuTab tab;
|
||||
|
||||
@override
|
||||
State<_Segment> createState() => _SegmentState();
|
||||
}
|
||||
|
||||
class _SegmentState extends State<_Segment> {
|
||||
bool isHovered = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Color? textColor = Theme.of(context).hintColor;
|
||||
if (isHovered) {
|
||||
textColor = const Color(0xFF00BCF0);
|
||||
} else if (widget.isSelected) {
|
||||
textColor = null;
|
||||
}
|
||||
|
||||
Widget child = MouseRegion(
|
||||
onEnter: (_) => setState(() => isHovered = true),
|
||||
onExit: (_) => setState(() => isHovered = false),
|
||||
child: FlowyText(
|
||||
widget.tab.i18n,
|
||||
textAlign: TextAlign.center,
|
||||
color: textColor,
|
||||
),
|
||||
);
|
||||
|
||||
if (widget.tab == ShareMenuTab.publish) {
|
||||
final isPublished = context.watch<ShareBloc>().state.isPublished;
|
||||
// show checkmark icon if published
|
||||
if (isPublished) {
|
||||
child = Row(
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.published_checkmark_s,
|
||||
blendMode: null,
|
||||
),
|
||||
const HSpace(6),
|
||||
child,
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,18 @@
|
||||
export "./src/sizes.dart";
|
||||
export "./src/trash_cell.dart";
|
||||
export "./src/trash_header.dart";
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
|
||||
import 'trash_page.dart';
|
||||
|
||||
export "./src/sizes.dart";
|
||||
export "./src/trash_cell.dart";
|
||||
export "./src/trash_header.dart";
|
||||
|
||||
class TrashPluginBuilder extends PluginBuilder {
|
||||
@override
|
||||
Plugin build(dynamic data) {
|
||||
@@ -66,6 +66,7 @@ class TrashPluginDisplay extends PluginWidgetBuilder {
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) =>
|
||||
const TrashPage(
|
||||
key: ValueKey('TrashPage'),
|
||||
|
||||
@@ -35,6 +35,9 @@ enum FeatureFlag {
|
||||
// used for controlling whether to show plan+billing options in settings
|
||||
planBilling,
|
||||
|
||||
// used for space design
|
||||
spaceDesign,
|
||||
|
||||
// used for ignore the conflicted feature flag
|
||||
unknown;
|
||||
|
||||
@@ -88,6 +91,9 @@ enum FeatureFlag {
|
||||
|
||||
bool get isOn {
|
||||
if ([
|
||||
// FeatureFlag.planBilling,
|
||||
// release this feature in version 0.6.1
|
||||
FeatureFlag.spaceDesign,
|
||||
// release this feature in version 0.5.9
|
||||
FeatureFlag.search,
|
||||
// release this feature in version 0.5.6
|
||||
@@ -105,15 +111,16 @@ enum FeatureFlag {
|
||||
}
|
||||
|
||||
switch (this) {
|
||||
case FeatureFlag.collaborativeWorkspace:
|
||||
case FeatureFlag.membersSettings:
|
||||
case FeatureFlag.planBilling:
|
||||
case FeatureFlag.unknown:
|
||||
return false;
|
||||
case FeatureFlag.search:
|
||||
case FeatureFlag.syncDocument:
|
||||
case FeatureFlag.syncDatabase:
|
||||
case FeatureFlag.spaceDesign:
|
||||
return true;
|
||||
case FeatureFlag.collaborativeWorkspace:
|
||||
case FeatureFlag.membersSettings:
|
||||
case FeatureFlag.unknown:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +138,8 @@ enum FeatureFlag {
|
||||
return 'if it\'s on, the command palette and search button will be available';
|
||||
case FeatureFlag.planBilling:
|
||||
return 'if it\'s on, plan and billing pages will be available in Settings';
|
||||
case FeatureFlag.spaceDesign:
|
||||
return 'if it\'s on, the space design feature will be available';
|
||||
case FeatureFlag.unknown:
|
||||
return '';
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/parsers/markdown_code_parser.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
|
||||
Document customMarkdownToDocument(String markdown) {
|
||||
return markdownToDocument(
|
||||
markdown,
|
||||
markdownParsers: [
|
||||
const MarkdownCodeBlockParser(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -182,8 +182,8 @@ void _resolveHomeDeps(GetIt getIt) {
|
||||
);
|
||||
|
||||
// share
|
||||
getIt.registerFactoryParam<DocumentShareBloc, ViewPB, void>(
|
||||
(view, _) => DocumentShareBloc(view: view),
|
||||
getIt.registerFactoryParam<ShareBloc, ViewPB, void>(
|
||||
(view, _) => ShareBloc(view: view),
|
||||
);
|
||||
|
||||
getIt.registerSingleton<ActionNavigationBloc>(ActionNavigationBloc());
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
library flowy_plugin;
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/user_profile.pb.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
export "./src/sandbox.dart";
|
||||
|
||||
@@ -76,6 +75,7 @@ abstract class PluginWidgetBuilder with NavigationItem {
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,7 @@ class InitAppWidgetTask extends LaunchTask {
|
||||
Locale('el', 'GR'),
|
||||
Locale('fr', 'FR'),
|
||||
Locale('fr', 'CA'),
|
||||
Locale('he'),
|
||||
Locale('hu', 'HU'),
|
||||
Locale('id', 'ID'),
|
||||
Locale('it', 'IT'),
|
||||
|
||||
@@ -6,8 +6,8 @@ import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
|
||||
class WindowSizeManager {
|
||||
static const double minWindowHeight = 600.0;
|
||||
static const double minWindowWidth = 800.0;
|
||||
static const double minWindowHeight = 640.0;
|
||||
static const double minWindowWidth = 960.0;
|
||||
// Preventing failed assertion due to Texture Descriptor Validation
|
||||
static const double maxWindowHeight = 8192.0;
|
||||
static const double maxWindowWidth = 8192.0;
|
||||
|
||||
@@ -95,7 +95,9 @@ class AppFlowyCloudDeepLink {
|
||||
}
|
||||
|
||||
if (_isPaymentSuccessUri(uri)) {
|
||||
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess();
|
||||
Log.debug("Payment success deep link: ${uri.toString()}");
|
||||
final plan = uri.queryParameters['plan'];
|
||||
return getIt<SubscriptionSuccessListenable>().onPaymentSuccess(plan);
|
||||
}
|
||||
|
||||
return _isAuthCallbackDeepLink(uri).fold(
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
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/text_completion.dart';
|
||||
@@ -9,6 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/wid
|
||||
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:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart' as fixnum;
|
||||
|
||||
class AppFlowyAIService implements AIRepository {
|
||||
@@ -85,6 +87,15 @@ class CompletionStream {
|
||||
_port.handler = _controller.add;
|
||||
_subscription = _controller.stream.listen(
|
||||
(event) async {
|
||||
if (event == "AI_RESPONSE_LIMIT") {
|
||||
onError(
|
||||
AIError(
|
||||
message: LocaleKeys.sideBar_aiResponseLimit.tr(),
|
||||
code: AIErrorCode.aiResponseLimitExceeded,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (event.startsWith("start:")) {
|
||||
await onStart();
|
||||
}
|
||||
@@ -96,6 +107,7 @@ class CompletionStream {
|
||||
if (event.startsWith("finish:")) {
|
||||
await onEnd();
|
||||
}
|
||||
|
||||
if (event.startsWith("error:")) {
|
||||
onError(AIError(message: event.substring(6)));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'package:appflowy/workspace/application/settings/plan/workspace_subscription_ext.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
@@ -10,13 +11,20 @@ import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
|
||||
abstract class IUserBackendService {
|
||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(String workspaceId);
|
||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
||||
String workspaceId,
|
||||
SubscriptionPlanPB plan,
|
||||
String? reason,
|
||||
);
|
||||
Future<FlowyResult<PaymentLinkPB, FlowyError>> createSubscription(
|
||||
String workspaceId,
|
||||
SubscriptionPlanPB plan,
|
||||
);
|
||||
}
|
||||
|
||||
const _baseBetaUrl = 'https://beta.appflowy.com';
|
||||
const _baseProdUrl = 'https://appflowy.com';
|
||||
|
||||
class UserBackendService implements IUserBackendService {
|
||||
UserBackendService({required this.userId});
|
||||
|
||||
@@ -228,9 +236,10 @@ class UserBackendService implements IUserBackendService {
|
||||
return UserEventLeaveWorkspace(data).send();
|
||||
}
|
||||
|
||||
static Future<FlowyResult<RepeatedWorkspaceSubscriptionPB, FlowyError>>
|
||||
getWorkspaceSubscriptions() {
|
||||
return UserEventGetWorkspaceSubscriptions().send();
|
||||
static Future<FlowyResult<WorkspaceSubscriptionInfoPB, FlowyError>>
|
||||
getWorkspaceSubscriptionInfo(String workspaceId) {
|
||||
final params = UserWorkspaceIdPB.create()..workspaceId = workspaceId;
|
||||
return UserEventGetWorkspaceSubscriptionInfo(params).send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<WorkspaceMemberPB, FlowyError>>
|
||||
@@ -250,15 +259,37 @@ class UserBackendService implements IUserBackendService {
|
||||
..recurringInterval = RecurringIntervalPB.Year
|
||||
..workspaceSubscriptionPlan = plan
|
||||
..successUrl =
|
||||
'${getIt<AppFlowyCloudSharedEnv>().appflowyCloudConfig.base_url}/web/payment-success';
|
||||
'${kDebugMode ? _baseBetaUrl : _baseProdUrl}/after-payment?plan=${plan.toRecognizable()}';
|
||||
return UserEventSubscribeWorkspace(request).send();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<FlowyResult<void, FlowyError>> cancelSubscription(
|
||||
String workspaceId,
|
||||
) {
|
||||
final request = UserWorkspaceIdPB()..workspaceId = workspaceId;
|
||||
SubscriptionPlanPB plan, [
|
||||
String? reason,
|
||||
]) {
|
||||
final request = CancelWorkspaceSubscriptionPB()
|
||||
..workspaceId = workspaceId
|
||||
..plan = plan;
|
||||
|
||||
if (reason != null) {
|
||||
request.reason = reason;
|
||||
}
|
||||
|
||||
return UserEventCancelWorkspaceSubscription(request).send();
|
||||
}
|
||||
|
||||
Future<FlowyResult<void, FlowyError>> updateSubscriptionPeriod(
|
||||
String workspaceId,
|
||||
SubscriptionPlanPB plan,
|
||||
RecurringIntervalPB interval,
|
||||
) {
|
||||
final request = UpdateWorkspaceSubscriptionPaymentPeriodPB()
|
||||
..workspaceId = workspaceId
|
||||
..plan = plan
|
||||
..recurringInterval = interval;
|
||||
|
||||
return UserEventUpdateWorkspaceSubscriptionPaymentPeriod(request).send();
|
||||
}
|
||||
}
|
||||
+2
-10
@@ -54,16 +54,8 @@ class DesktopSignInScreen extends StatelessWidget {
|
||||
const SignInAnonymousButtonV2(),
|
||||
const VSpace(10),
|
||||
|
||||
SwitchSignInSignUpButton(
|
||||
onTap: () {
|
||||
final type = state.loginType == LoginType.signIn
|
||||
? LoginType.signUp
|
||||
: LoginType.signIn;
|
||||
context
|
||||
.read<SignInBloc>()
|
||||
.add(SignInEvent.switchLoginType(type));
|
||||
},
|
||||
),
|
||||
// sign in agreement
|
||||
const SignInAgreement(),
|
||||
|
||||
// loading status
|
||||
const VSpace(indicatorMinHeight),
|
||||
|
||||
+1
-10
@@ -39,16 +39,7 @@ class MobileSignInScreen extends StatelessWidget {
|
||||
const VSpace(spacing),
|
||||
const SignInAnonymousButtonV2(),
|
||||
const VSpace(spacing),
|
||||
SwitchSignInSignUpButton(
|
||||
onTap: () {
|
||||
final type = state.loginType == LoginType.signIn
|
||||
? LoginType.signUp
|
||||
: LoginType.signIn;
|
||||
context.read<SignInBloc>().add(
|
||||
SignInEvent.switchLoginType(type),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SignInAgreement(),
|
||||
const VSpace(spacing),
|
||||
_buildSettingsButton(context),
|
||||
if (!isAuthEnabled) const Spacer(flex: 2),
|
||||
|
||||
+11
-10
@@ -1,14 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/application/sign_in_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/toast.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
|
||||
class SignInWithMagicLinkButtons extends StatefulWidget {
|
||||
const SignInWithMagicLinkButtons({super.key});
|
||||
@@ -53,18 +53,19 @@ class _SignInWithMagicLinkButtonsState
|
||||
|
||||
void _sendMagicLink(BuildContext context, String email) {
|
||||
if (!isEmail(email)) {
|
||||
return showSnackBarMessage(
|
||||
return showToastNotification(
|
||||
context,
|
||||
LocaleKeys.signIn_invalidEmail.tr(),
|
||||
duration: const Duration(seconds: 8),
|
||||
message: LocaleKeys.signIn_invalidEmail.tr(),
|
||||
type: ToastificationType.error,
|
||||
);
|
||||
}
|
||||
|
||||
context.read<SignInBloc>().add(SignInEvent.signedWithMagicLink(email));
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.signIn_magicLinkSent.tr(),
|
||||
duration: const Duration(seconds: 1000),
|
||||
|
||||
showConfirmDialog(
|
||||
context: context,
|
||||
title: LocaleKeys.signIn_magicLinkSent.tr(),
|
||||
description: LocaleKeys.signIn_magicLinkSentDescription.tr(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import 'package:appflowy/core/helpers/url_launcher.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SignInAgreement extends StatelessWidget {
|
||||
const SignInAgreement({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.web_signInAgreement.tr()} ',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.web_termOfUse.tr()} ',
|
||||
style: const TextStyle(color: Colors.blue, fontSize: 12),
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => afLaunchUrlString('https://appflowy.io/terms'),
|
||||
),
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.web_and.tr()} ',
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
TextSpan(
|
||||
text: LocaleKeys.web_privacyPolicy.tr(),
|
||||
style: const TextStyle(color: Colors.blue, fontSize: 12),
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => afLaunchUrlString('https://appflowy.io/privacy'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
export 'magic_link_sign_in_buttons.dart';
|
||||
export 'sign_in_anonymous_button.dart';
|
||||
export 'sign_in_or_logout_button.dart';
|
||||
export 'switch_sign_in_sign_up_button.dart';
|
||||
|
||||
// export 'switch_sign_in_sign_up_button.dart';
|
||||
export 'third_party_sign_in_buttons.dart';
|
||||
export 'sign_in_agreement.dart';
|
||||
@@ -5,6 +5,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart'
|
||||
show WorkspaceSettingPB;
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'home_bloc.freezed.dart';
|
||||
|
||||
class HomeBloc extends Bloc<HomeEvent, HomeState> {
|
||||
|
||||
@@ -54,9 +54,11 @@ class SidebarSectionsBloc
|
||||
_initial(userProfile, workspaceId);
|
||||
final sectionViews = await _getSectionViews();
|
||||
if (sectionViews != null) {
|
||||
final containsSpace = _containsSpace(sectionViews);
|
||||
emit(
|
||||
state.copyWith(
|
||||
section: sectionViews,
|
||||
containsSpace: containsSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -65,9 +67,11 @@ class SidebarSectionsBloc
|
||||
_reset(userProfile, workspaceId);
|
||||
final sectionViews = await _getSectionViews();
|
||||
if (sectionViews != null) {
|
||||
final containsSpace = _containsSpace(sectionViews);
|
||||
emit(
|
||||
state.copyWith(
|
||||
section: sectionViews,
|
||||
containsSpace: containsSpace,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -102,6 +106,8 @@ class SidebarSectionsBloc
|
||||
case ViewSectionPB.Public:
|
||||
emit(
|
||||
state.copyWith(
|
||||
containsSpace: state.containsSpace ||
|
||||
sectionViews.views.any((view) => view.isSpace),
|
||||
section: state.section.copyWith(
|
||||
publicViews: sectionViews.views,
|
||||
),
|
||||
@@ -110,6 +116,8 @@ class SidebarSectionsBloc
|
||||
case ViewSectionPB.Private:
|
||||
emit(
|
||||
state.copyWith(
|
||||
containsSpace: state.containsSpace ||
|
||||
sectionViews.views.any((view) => view.isSpace),
|
||||
section: state.section.copyWith(
|
||||
privateViews: sectionViews.views,
|
||||
),
|
||||
@@ -160,9 +168,11 @@ class SidebarSectionsBloc
|
||||
_initial(userProfile, workspaceId);
|
||||
final sectionViews = await _getSectionViews();
|
||||
if (sectionViews != null) {
|
||||
final containsSpace = _containsSpace(sectionViews);
|
||||
emit(
|
||||
state.copyWith(
|
||||
section: sectionViews,
|
||||
containsSpace: containsSpace,
|
||||
),
|
||||
);
|
||||
// try to open the fist view in public section or private section
|
||||
@@ -229,6 +239,11 @@ class SidebarSectionsBloc
|
||||
}
|
||||
}
|
||||
|
||||
bool _containsSpace(SidebarSection section) {
|
||||
return section.publicViews.any((view) => view.isSpace) ||
|
||||
section.privateViews.any((view) => view.isSpace);
|
||||
}
|
||||
|
||||
void _initial(UserProfilePB userProfile, String workspaceId) {
|
||||
_workspaceService = WorkspaceService(workspaceId: workspaceId);
|
||||
|
||||
@@ -292,6 +307,7 @@ class SidebarSectionsState with _$SidebarSectionsState {
|
||||
required SidebarSection section,
|
||||
@Default(null) ViewPB? lastCreatedRootView,
|
||||
FlowyResult<void, FlowyError>? createRootViewResult,
|
||||
@Default(true) bool containsSpace,
|
||||
}) = _SidebarSectionsState;
|
||||
|
||||
factory SidebarSectionsState.initial() => const SidebarSectionsState(
|
||||
|
||||
+6
-3
@@ -1,5 +1,6 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/shared/list_extension.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_listener.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
@@ -7,6 +8,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:fixnum/fixnum.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// This is a lazy-singleton to share recent views across the application.
|
||||
@@ -37,7 +39,7 @@ class CachedRecentService {
|
||||
|
||||
_listener.start(recentViewsUpdated: _recentViewsUpdated);
|
||||
_recentViews = await _readRecentViews().fold(
|
||||
(s) => s.items,
|
||||
(s) => s.items.unique((e) => e.item.id),
|
||||
(_) => [],
|
||||
);
|
||||
_completer.complete();
|
||||
@@ -68,7 +70,8 @@ class CachedRecentService {
|
||||
|
||||
Future<FlowyResult<RepeatedRecentViewPB, FlowyError>>
|
||||
_readRecentViews() async {
|
||||
final result = await FolderEventReadRecentViews().send();
|
||||
final payload = ReadRecentViewsPB(start: Int64(), limit: Int64(100));
|
||||
final result = await FolderEventReadRecentViews(payload).send();
|
||||
return result.fold(
|
||||
(recentViews) {
|
||||
return FlowyResult.success(
|
||||
@@ -101,7 +104,7 @@ class CachedRecentService {
|
||||
final viewIds = result.toNullable();
|
||||
if (viewIds != null) {
|
||||
_recentViews = await _readRecentViews().fold(
|
||||
(s) => s.items,
|
||||
(s) => s.items.unique((e) => e.item.id),
|
||||
(_) => [],
|
||||
);
|
||||
}
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import 'dart:async';
|
||||
import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/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:fixnum/fixnum.dart';
|
||||
part 'download_model_bloc.freezed.dart';
|
||||
|
||||
class DownloadModelBloc extends Bloc<DownloadModelEvent, DownloadModelState> {
|
||||
DownloadModelBloc(LLMModelPB model)
|
||||
: super(DownloadModelState(model: model)) {
|
||||
on<DownloadModelEvent>(_handleEvent);
|
||||
}
|
||||
|
||||
Future<void> _handleEvent(
|
||||
DownloadModelEvent event,
|
||||
Emitter<DownloadModelState> emit,
|
||||
) async {
|
||||
await event.when(
|
||||
started: () async {
|
||||
final downloadStream = DownloadingStream();
|
||||
downloadStream.listen(
|
||||
onModelPercentage: (name, percent) {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
DownloadModelEvent.updatePercent(name, percent),
|
||||
);
|
||||
}
|
||||
},
|
||||
onPluginPercentage: (percent) {
|
||||
if (!isClosed) {
|
||||
add(DownloadModelEvent.updatePercent("AppFlowy Plugin", percent));
|
||||
}
|
||||
},
|
||||
onFinish: () {
|
||||
add(const DownloadModelEvent.downloadFinish());
|
||||
},
|
||||
onError: (err) {
|
||||
Log.error(err);
|
||||
},
|
||||
);
|
||||
|
||||
final payload =
|
||||
DownloadLLMPB(progressStream: Int64(downloadStream.nativePort));
|
||||
final result = await ChatEventDownloadLLMResource(payload).send();
|
||||
result.fold((_) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
downloadStream: downloadStream,
|
||||
loadingState: const LoadingState.finish(),
|
||||
downloadError: null,
|
||||
),
|
||||
);
|
||||
}, (err) {
|
||||
emit(state.copyWith(loadingState: LoadingState.finish(error: err)));
|
||||
});
|
||||
},
|
||||
updatePercent: (String object, double percent) {
|
||||
emit(state.copyWith(object: object, percent: percent));
|
||||
},
|
||||
downloadFinish: () {
|
||||
emit(state.copyWith(isFinish: true));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await state.downloadStream?.dispose();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DownloadModelEvent with _$DownloadModelEvent {
|
||||
const factory DownloadModelEvent.started() = _Started;
|
||||
const factory DownloadModelEvent.updatePercent(
|
||||
String object,
|
||||
double percent,
|
||||
) = _UpdatePercent;
|
||||
const factory DownloadModelEvent.downloadFinish() = _DownloadFinish;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class DownloadModelState with _$DownloadModelState {
|
||||
const factory DownloadModelState({
|
||||
required LLMModelPB model,
|
||||
DownloadingStream? downloadStream,
|
||||
String? downloadError,
|
||||
@Default("") String object,
|
||||
@Default(0) double percent,
|
||||
@Default(false) bool isFinish,
|
||||
@Default(LoadingState.loading()) LoadingState loadingState,
|
||||
}) = _DownloadModelState;
|
||||
}
|
||||
|
||||
class DownloadingStream {
|
||||
DownloadingStream() {
|
||||
_port.handler = _controller.add;
|
||||
}
|
||||
|
||||
final RawReceivePort _port = RawReceivePort();
|
||||
StreamSubscription<String>? _sub;
|
||||
final StreamController<String> _controller = StreamController.broadcast();
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
|
||||
Future<void> dispose() async {
|
||||
await _sub?.cancel();
|
||||
await _controller.close();
|
||||
_port.close();
|
||||
}
|
||||
|
||||
void listen({
|
||||
void Function(String modelName, double percent)? onModelPercentage,
|
||||
void Function(double percent)? onPluginPercentage,
|
||||
void Function(String data)? onError,
|
||||
void Function()? onFinish,
|
||||
}) {
|
||||
_sub = _controller.stream.listen((text) {
|
||||
if (text.contains(':progress:')) {
|
||||
final progressIndex = text.indexOf(':progress:');
|
||||
final modelName = text.substring(0, progressIndex);
|
||||
final progressValue = text
|
||||
.substring(progressIndex + 10); // 10 is the length of ":progress:"
|
||||
final percent = double.tryParse(progressValue);
|
||||
if (percent != null) {
|
||||
onModelPercentage?.call(modelName, percent);
|
||||
}
|
||||
} else if (text.startsWith('plugin:progress:')) {
|
||||
final percent = double.tryParse(text.substring(16));
|
||||
if (percent != null) {
|
||||
onPluginPercentage?.call(percent);
|
||||
}
|
||||
} else if (text.startsWith('finish')) {
|
||||
onFinish?.call();
|
||||
} else if (text.startsWith('error:')) {
|
||||
// substring 6 to remove "error:"
|
||||
onError?.call(text.substring(6));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'local_ai_bloc.freezed.dart';
|
||||
|
||||
class LocalAIToggleBloc extends Bloc<LocalAIToggleEvent, LocalAIToggleState> {
|
||||
LocalAIToggleBloc() : super(const LocalAIToggleState()) {
|
||||
on<LocalAIToggleEvent>(_handleEvent);
|
||||
}
|
||||
|
||||
Future<void> _handleEvent(
|
||||
LocalAIToggleEvent event,
|
||||
Emitter<LocalAIToggleState> emit,
|
||||
) async {
|
||||
await event.when(
|
||||
started: () async {
|
||||
final result = await ChatEventGetLocalAIState().send();
|
||||
_handleResult(emit, result);
|
||||
},
|
||||
toggle: () async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
pageIndicator: const LocalAIToggleStateIndicator.loading(),
|
||||
),
|
||||
);
|
||||
unawaited(
|
||||
ChatEventToggleLocalAI().send().then(
|
||||
(result) {
|
||||
if (!isClosed) {
|
||||
add(LocalAIToggleEvent.handleResult(result));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
handleResult: (result) {
|
||||
_handleResult(emit, result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleResult(
|
||||
Emitter<LocalAIToggleState> emit,
|
||||
FlowyResult<LocalAIPB, FlowyError> result,
|
||||
) {
|
||||
result.fold(
|
||||
(localAI) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
pageIndicator: LocalAIToggleStateIndicator.ready(localAI.enabled),
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
pageIndicator: LocalAIToggleStateIndicator.error(err),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIToggleEvent with _$LocalAIToggleEvent {
|
||||
const factory LocalAIToggleEvent.started() = _Started;
|
||||
const factory LocalAIToggleEvent.toggle() = _Toggle;
|
||||
const factory LocalAIToggleEvent.handleResult(
|
||||
FlowyResult<LocalAIPB, FlowyError> result,
|
||||
) = _HandleResult;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIToggleState with _$LocalAIToggleState {
|
||||
const factory LocalAIToggleState({
|
||||
@Default(LocalAIToggleStateIndicator.loading())
|
||||
LocalAIToggleStateIndicator pageIndicator,
|
||||
}) = _LocalAIToggleState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIToggleStateIndicator with _$LocalAIToggleStateIndicator {
|
||||
// when start downloading the model
|
||||
const factory LocalAIToggleStateIndicator.error(FlowyError error) = _OnError;
|
||||
const factory LocalAIToggleStateIndicator.ready(bool isEnabled) = _Ready;
|
||||
const factory LocalAIToggleStateIndicator.loading() = _Loading;
|
||||
}
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/settings/ai/local_llm_listener.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'local_ai_chat_bloc.freezed.dart';
|
||||
|
||||
class LocalAIChatSettingBloc
|
||||
extends Bloc<LocalAIChatSettingEvent, LocalAIChatSettingState> {
|
||||
LocalAIChatSettingBloc()
|
||||
: listener = LocalLLMListener(),
|
||||
super(const LocalAIChatSettingState()) {
|
||||
listener.start(
|
||||
stateCallback: (newState) {
|
||||
if (!isClosed) {
|
||||
add(LocalAIChatSettingEvent.updateLLMRunningState(newState.state));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
on<LocalAIChatSettingEvent>(_handleEvent);
|
||||
}
|
||||
|
||||
final LocalLLMListener listener;
|
||||
|
||||
/// Handles incoming events and dispatches them to the appropriate handler.
|
||||
Future<void> _handleEvent(
|
||||
LocalAIChatSettingEvent event,
|
||||
Emitter<LocalAIChatSettingState> emit,
|
||||
) async {
|
||||
await event.when(
|
||||
refreshAISetting: _handleStarted,
|
||||
didLoadModelInfo: (FlowyResult<LLMModelInfoPB, FlowyError> result) {
|
||||
result.fold(
|
||||
(modelInfo) {
|
||||
_fetchCurremtLLMState();
|
||||
emit(
|
||||
state.copyWith(
|
||||
modelInfo: modelInfo,
|
||||
models: modelInfo.models,
|
||||
selectedLLMModel: modelInfo.selectedModel,
|
||||
fetchModelInfoState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
fetchModelInfoState: LoadingState.finish(error: err),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
selectLLMConfig: (LLMModelPB llmModel) async {
|
||||
final result = await ChatEventUpdateLocalLLM(llmModel).send();
|
||||
result.fold(
|
||||
(llmResource) {
|
||||
// If all resources are downloaded, show reload plugin
|
||||
if (llmResource.pendingResources.isNotEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedLLMModel: llmModel,
|
||||
localAIInfo: LocalAIProgress.showDownload(
|
||||
llmResource,
|
||||
llmModel,
|
||||
),
|
||||
selectLLMState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedLLMModel: llmModel,
|
||||
selectLLMState: const LoadingState.finish(),
|
||||
localAIInfo: const LocalAIProgress.checkPluginState(),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectLLMState: LoadingState.finish(error: err),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
refreshLLMState: (LocalModelResourcePB llmResource) {
|
||||
if (state.selectedLLMModel == null) {
|
||||
Log.error(
|
||||
'Unexpected null selected config. It should be set already',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// reload plugin if all resources are downloaded
|
||||
if (llmResource.pendingResources.isEmpty) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
localAIInfo: const LocalAIProgress.checkPluginState(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (state.selectedLLMModel != null) {
|
||||
// Go to download page if the selected model is downloading
|
||||
if (llmResource.isDownloading) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
localAIInfo:
|
||||
LocalAIProgress.startDownloading(state.selectedLLMModel!),
|
||||
selectLLMState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
return;
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
localAIInfo: LocalAIProgress.showDownload(
|
||||
llmResource,
|
||||
state.selectedLLMModel!,
|
||||
),
|
||||
selectLLMState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
startDownloadModel: (LLMModelPB llmModel) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
localAIInfo: LocalAIProgress.startDownloading(llmModel),
|
||||
selectLLMState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
},
|
||||
cancelDownload: () async {
|
||||
final _ = await ChatEventCancelDownloadLLMResource().send();
|
||||
_fetchCurremtLLMState();
|
||||
},
|
||||
finishDownload: () async {
|
||||
emit(
|
||||
state.copyWith(localAIInfo: const LocalAIProgress.finishDownload()),
|
||||
);
|
||||
},
|
||||
updateLLMRunningState: (RunningStatePB newRunningState) {
|
||||
if (newRunningState == RunningStatePB.Stopped) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
runningState: newRunningState,
|
||||
localAIInfo: const LocalAIProgress.checkPluginState(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(state.copyWith(runningState: newRunningState));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _fetchCurremtLLMState() async {
|
||||
final result = await ChatEventGetLocalLLMState().send();
|
||||
result.fold(
|
||||
(llmResource) {
|
||||
if (!isClosed) {
|
||||
add(LocalAIChatSettingEvent.refreshLLMState(llmResource));
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
Log.error(err);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// Handles the event to fetch local AI settings when the application starts.
|
||||
Future<void> _handleStarted() async {
|
||||
final result = await ChatEventRefreshLocalAIModelInfo().send();
|
||||
if (!isClosed) {
|
||||
add(LocalAIChatSettingEvent.didLoadModelInfo(result));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await listener.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIChatSettingEvent with _$LocalAIChatSettingEvent {
|
||||
const factory LocalAIChatSettingEvent.refreshAISetting() = _RefreshAISetting;
|
||||
const factory LocalAIChatSettingEvent.didLoadModelInfo(
|
||||
FlowyResult<LLMModelInfoPB, FlowyError> result,
|
||||
) = _ModelInfo;
|
||||
const factory LocalAIChatSettingEvent.selectLLMConfig(LLMModelPB config) =
|
||||
_SelectLLMConfig;
|
||||
|
||||
const factory LocalAIChatSettingEvent.refreshLLMState(
|
||||
LocalModelResourcePB llmResource,
|
||||
) = _RefreshLLMResource;
|
||||
const factory LocalAIChatSettingEvent.startDownloadModel(
|
||||
LLMModelPB llmModel,
|
||||
) = _StartDownloadModel;
|
||||
|
||||
const factory LocalAIChatSettingEvent.cancelDownload() = _CancelDownload;
|
||||
const factory LocalAIChatSettingEvent.finishDownload() = _FinishDownload;
|
||||
const factory LocalAIChatSettingEvent.updateLLMRunningState(
|
||||
RunningStatePB newRunningState,
|
||||
) = _RunningState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIChatSettingState with _$LocalAIChatSettingState {
|
||||
const factory LocalAIChatSettingState({
|
||||
LLMModelInfoPB? modelInfo,
|
||||
LLMModelPB? selectedLLMModel,
|
||||
LocalAIProgress? localAIInfo,
|
||||
@Default(LoadingState.loading()) LoadingState fetchModelInfoState,
|
||||
@Default(LoadingState.loading()) LoadingState selectLLMState,
|
||||
@Default([]) List<LLMModelPB> models,
|
||||
@Default(RunningStatePB.Connecting) RunningStatePB runningState,
|
||||
}) = _LocalAIChatSettingState;
|
||||
}
|
||||
|
||||
// @freezed
|
||||
// class LocalChatAIStateIndicator with _$LocalChatAIStateIndicator {
|
||||
// // when start downloading the model
|
||||
// const factory LocalChatAIStateIndicator.error(FlowyError error) = _OnError;
|
||||
// const factory LocalChatAIStateIndicator.ready(bool isEnabled) = _Ready;
|
||||
// }
|
||||
|
||||
@freezed
|
||||
class LocalAIProgress with _$LocalAIProgress {
|
||||
// when user select a new model, it will call requestDownload
|
||||
const factory LocalAIProgress.requestDownloadInfo(
|
||||
LocalModelResourcePB llmResource,
|
||||
LLMModelPB llmModel,
|
||||
) = _RequestDownload;
|
||||
|
||||
// when user comes back to the setting page, it will auto detect current llm state
|
||||
const factory LocalAIProgress.showDownload(
|
||||
LocalModelResourcePB llmResource,
|
||||
LLMModelPB llmModel,
|
||||
) = _DownloadNeeded;
|
||||
|
||||
// when start downloading the model
|
||||
const factory LocalAIProgress.startDownloading(LLMModelPB llmModel) =
|
||||
_Downloading;
|
||||
const factory LocalAIProgress.finishDownload() = _Finish;
|
||||
const factory LocalAIProgress.checkPluginState() = _PluginState;
|
||||
}
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
part 'local_ai_chat_toggle_bloc.freezed.dart';
|
||||
|
||||
class LocalAIChatToggleBloc
|
||||
extends Bloc<LocalAIChatToggleEvent, LocalAIChatToggleState> {
|
||||
LocalAIChatToggleBloc() : super(const LocalAIChatToggleState()) {
|
||||
on<LocalAIChatToggleEvent>(_handleEvent);
|
||||
}
|
||||
|
||||
Future<void> _handleEvent(
|
||||
LocalAIChatToggleEvent event,
|
||||
Emitter<LocalAIChatToggleState> emit,
|
||||
) async {
|
||||
await event.when(
|
||||
started: () async {
|
||||
final result = await ChatEventGetLocalAIChatState().send();
|
||||
_handleResult(emit, result);
|
||||
},
|
||||
toggle: () async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
pageIndicator: const LocalAIChatToggleStateIndicator.loading(),
|
||||
),
|
||||
);
|
||||
unawaited(
|
||||
ChatEventToggleLocalAIChat().send().then(
|
||||
(result) {
|
||||
if (!isClosed) {
|
||||
add(LocalAIChatToggleEvent.handleResult(result));
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
handleResult: (result) {
|
||||
_handleResult(emit, result);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _handleResult(
|
||||
Emitter<LocalAIChatToggleState> emit,
|
||||
FlowyResult<LocalAIChatPB, FlowyError> result,
|
||||
) {
|
||||
result.fold(
|
||||
(localAI) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
pageIndicator:
|
||||
LocalAIChatToggleStateIndicator.ready(localAI.enabled),
|
||||
),
|
||||
);
|
||||
},
|
||||
(err) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
pageIndicator: LocalAIChatToggleStateIndicator.error(err),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIChatToggleEvent with _$LocalAIChatToggleEvent {
|
||||
const factory LocalAIChatToggleEvent.started() = _Started;
|
||||
const factory LocalAIChatToggleEvent.toggle() = _Toggle;
|
||||
const factory LocalAIChatToggleEvent.handleResult(
|
||||
FlowyResult<LocalAIChatPB, FlowyError> result,
|
||||
) = _HandleResult;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIChatToggleState with _$LocalAIChatToggleState {
|
||||
const factory LocalAIChatToggleState({
|
||||
@Default(LocalAIChatToggleStateIndicator.loading())
|
||||
LocalAIChatToggleStateIndicator pageIndicator,
|
||||
}) = _LocalAIChatToggleState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class LocalAIChatToggleStateIndicator with _$LocalAIChatToggleStateIndicator {
|
||||
const factory LocalAIChatToggleStateIndicator.error(FlowyError error) =
|
||||
_OnError;
|
||||
const factory LocalAIChatToggleStateIndicator.ready(bool isEnabled) = _Ready;
|
||||
const factory LocalAIChatToggleStateIndicator.loading() = _Loading;
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
import 'dart:async';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_notification.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-chat/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-notification/subject.pb.dart';
|
||||
import 'package:appflowy_backend/rust_stream.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
typedef PluginStateCallback = void Function(LocalAIPluginStatePB state);
|
||||
typedef LocalAIChatCallback = void Function(LocalAIChatPB chatState);
|
||||
|
||||
class LocalLLMListener {
|
||||
LocalLLMListener() {
|
||||
_parser =
|
||||
ChatNotificationParser(id: "appflowy_ai_plugin", callback: _callback);
|
||||
_subscription = RustStreamReceiver.listen(
|
||||
(observable) => _parser?.parse(observable),
|
||||
);
|
||||
}
|
||||
|
||||
StreamSubscription<SubscribeObject>? _subscription;
|
||||
ChatNotificationParser? _parser;
|
||||
|
||||
PluginStateCallback? stateCallback;
|
||||
LocalAIChatCallback? chatStateCallback;
|
||||
void Function()? finishStreamingCallback;
|
||||
|
||||
void start({
|
||||
PluginStateCallback? stateCallback,
|
||||
LocalAIChatCallback? chatStateCallback,
|
||||
}) {
|
||||
this.stateCallback = stateCallback;
|
||||
this.chatStateCallback = chatStateCallback;
|
||||
}
|
||||
|
||||
void _callback(
|
||||
ChatNotification ty,
|
||||
FlowyResult<Uint8List, FlowyError> result,
|
||||
) {
|
||||
result.map((r) {
|
||||
switch (ty) {
|
||||
case ChatNotification.UpdateChatPluginState:
|
||||
stateCallback?.call(LocalAIPluginStatePB.fromBuffer(r));
|
||||
break;
|
||||
case ChatNotification.UpdateLocalChatAI:
|
||||
chatStateCallback?.call(LocalAIChatPB.fromBuffer(r));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> stop() async {
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
}
|
||||
}
|
||||
Loaded 100 of 685 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user