Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c3caa319e2 |
No files matched your search
@@ -0,0 +1,72 @@
|
||||
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 }}
|
||||
|
||||
@@ -248,13 +248,10 @@ jobs:
|
||||
env:
|
||||
BACKEND_VERSION: 0.3.24-amd64
|
||||
run: |
|
||||
if [ "$(docker ps --filter name=appflowy-cloud -q)" == "" ]; then
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
else
|
||||
echo "Docker container 'appflowy-cloud' is already running."
|
||||
fi
|
||||
docker compose down -v --remove-orphans
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -28,35 +28,20 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
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
|
||||
build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ macos-14 ]
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
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 }}
|
||||
@@ -64,7 +49,8 @@ jobs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install Flutter
|
||||
- name: Install flutter
|
||||
id: flutter
|
||||
uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: "stable"
|
||||
@@ -73,7 +59,7 @@ jobs:
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
prefix-key: macos-latest
|
||||
prefix-key: ${{ matrix.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
|
||||
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
job:
|
||||
- { target: x86_64-apple-darwin, os: macos-12, extra-build-args: "" }
|
||||
- { target: x86_64-apple-darwin, os: macos-11, 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-latest,
|
||||
os: macos-11,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
@@ -479,24 +479,6 @@ jobs:
|
||||
cache-from: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache,mode=max
|
||||
|
||||
notify-failure:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- build-for-macOS-x86_64
|
||||
- build-for-windows
|
||||
- build-for-linux
|
||||
if: failure()
|
||||
steps:
|
||||
- uses: 8398a7/action-slack@v3
|
||||
with:
|
||||
status: ${{ job.status }}
|
||||
text: |
|
||||
🔴🔴🔴Workflow ${{ github.workflow }} in repository ${{ github.repository }} was failed 🔴🔴🔴.
|
||||
fields: repo,message,author,eventName,ref,workflow
|
||||
env:
|
||||
SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }}
|
||||
if: always()
|
||||
|
||||
notify-discord:
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
|
||||
@@ -8,7 +8,6 @@ on:
|
||||
- "release/*"
|
||||
paths:
|
||||
- "frontend/rust-lib/**"
|
||||
- ".github/workflows/rust_ci.yaml"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -23,49 +22,17 @@ env:
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
|
||||
jobs:
|
||||
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
|
||||
test-on-ubuntu:
|
||||
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
|
||||
@@ -78,16 +45,23 @@ 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: ${{ runner.os }}
|
||||
cache-on-failure: true
|
||||
prefix-key: "ubuntu-latest"
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
|
||||
@@ -100,20 +74,18 @@ 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
|
||||
|
||||
- name: Run Docker-Compose
|
||||
working-directory: AppFlowy-Cloud
|
||||
env:
|
||||
BACKEND_VERSION: 0.3.24-amd64
|
||||
run: |
|
||||
if [ "$(docker ps --filter name=appflowy-cloud -q)" == "" ]; then
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
sleep 10
|
||||
else
|
||||
echo "Docker container 'appflowy-cloud' is already running."
|
||||
fi
|
||||
docker pull appflowyinc/appflowy_cloud:latest
|
||||
docker compose up -d
|
||||
|
||||
- name: Run rust-lib tests
|
||||
working-directory: frontend/rust-lib
|
||||
@@ -134,12 +106,6 @@ 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,5 +1,4 @@
|
||||
name: Tauri-CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
@@ -12,47 +11,28 @@ env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
CARGO_MAKE_VERSION: "0.36.6"
|
||||
CI: true
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tauri-build-self-hosted:
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
runs-on: self-hosted
|
||||
tauri-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ ubuntu-20.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
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-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
@@ -81,27 +61,36 @@ jobs:
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./frontend/appflowy_web_app/src-tauri -> target"
|
||||
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_web_app/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install dependencies
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == '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'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
|
||||
|
||||
- name: install tauri deps tools
|
||||
- name: install cargo-make
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force cargo-make
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
shell: bash
|
||||
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_web_app
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
name: WEB-CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
build:
|
||||
description: 'Build the web app'
|
||||
required: true
|
||||
default: 'true'
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.77.2"
|
||||
CARGO_MAKE_VERSION: "0.36.6"
|
||||
|
||||
jobs:
|
||||
web-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ ubuntu-latest ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: setup node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
|
||||
- name: Cache Rust Dependencies
|
||||
uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: rust-dependencies-${{ runner.os }}
|
||||
workspaces: |
|
||||
frontend/rust-lib
|
||||
frontend/appflowy_web/appflowy_wasm
|
||||
|
||||
# TODO: Can combine caching deps and node_modules in one
|
||||
# See Glob patterns: https://github.com/actions/toolkit/tree/main/packages/glob
|
||||
- name: Cache Node.js dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.npm
|
||||
key: npm-${{ runner.os }}
|
||||
|
||||
- name: Cache node_modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: frontend/appflowy_web/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Install wasm-pack
|
||||
run: cargo install wasm-pack
|
||||
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
|
||||
|
||||
- name: install dependencies
|
||||
if: matrix.platform == 'ubuntu-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
sudo apt-get update
|
||||
npm install -g pnpm@${{ env.PNPM_VERSION }}
|
||||
cargo make install_web_protobuf
|
||||
|
||||
- name: Build
|
||||
working-directory: frontend/appflowy_web
|
||||
run: |
|
||||
pnpm install
|
||||
pnpm run build_release_wasm
|
||||
@@ -1,37 +1,4 @@
|
||||
# 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.
|
||||
- Added support for moving pages across spaces.
|
||||
- Undo markdown formatting with `Ctrl + Z` or `Cmd + Z`.
|
||||
- Improved shortcuts settings UI.
|
||||
### Bug Fixes
|
||||
- Fixed unable to zoom in with `Ctrl` and `+` or `Cmd` and `+` on some keyboards.
|
||||
- Fixed unable to paste nested lists in existing lists.
|
||||
|
||||
## Version 0.6.1 - 22/06/2024
|
||||
### New Features
|
||||
- Introduced the "Space" feature to help you organize your pages more efficiently.
|
||||
### Bug Fixes
|
||||
- Resolved shortcut conflicts on the board page.
|
||||
- Resolved an issue where underscores could cause the editor to freeze.
|
||||
|
||||
## Version 0.6.0 - 19/06/2024
|
||||
### New Features
|
||||
- Introduced the "Space" feature to help you organize your pages more efficiently.
|
||||
|
||||
Vendored
+2
-1
@@ -13,6 +13,7 @@
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
@@ -137,4 +138,4 @@
|
||||
"cwd": "${workspaceRoot}/appflowy_tauri/"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
|
||||
CARGO_MAKE_CRATE_NAME = "dart-ffi"
|
||||
LIB_NAME = "dart_ffi"
|
||||
APPFLOWY_VERSION = "0.6.6"
|
||||
APPFLOWY_VERSION = "0.6.1"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
@@ -50,6 +50,7 @@ APP_ENVIRONMENT = "local"
|
||||
FLUTTER_FLOWY_SDK_PATH = "appflowy_flutter/packages/appflowy_backend"
|
||||
TAURI_BACKEND_SERVICE_PATH = "appflowy_tauri/src/services/backend"
|
||||
WEB_BACKEND_SERVICE_PATH = "appflowy_web/src/services/backend"
|
||||
WEB_LIB_PATH = "appflowy_web/wasm-libs/af-wasm"
|
||||
TAURI_APP_BACKEND_SERVICE_PATH = "appflowy_web_app/src/application/services/tauri-services/backend"
|
||||
# Test default config
|
||||
TEST_CRATE_TYPE = "cdylib"
|
||||
|
||||
@@ -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 34
|
||||
targetSdkVersion 33
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -42,7 +42,7 @@ void main() {
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// rename the name of the anon user
|
||||
// reanme the name of the anon user
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
@@ -47,11 +47,11 @@ void main() {
|
||||
);
|
||||
await tester.tapButton(find.byType(SignInOutButton));
|
||||
|
||||
tester.expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
tester.expectToSeeText(LocaleKeys.button_confirm.tr());
|
||||
await tester.tapButtonWithName(LocaleKeys.button_confirm.tr());
|
||||
|
||||
// Go to the sign in page again
|
||||
await tester.pumpAndSettle(const Duration(seconds: 5));
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
tester.expectToSeeGoogleLoginButton();
|
||||
});
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@ void main() {
|
||||
await tester.ime.insertText(inputContent);
|
||||
expect(find.text(inputContent, findRichText: true), findsOneWidget);
|
||||
|
||||
// TODO(nathan): remove the await
|
||||
// 6 seconds for data sync
|
||||
await tester.waitForSeconds(6);
|
||||
|
||||
|
||||
@@ -50,28 +50,26 @@ void main() {
|
||||
await tester.tapEscButton();
|
||||
|
||||
// wait 2 seconds for the sync to finish
|
||||
await tester.pumpAndSettle(const Duration(seconds: 6));
|
||||
});
|
||||
|
||||
|
||||
testWidgets('get user icon and name from server', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
email: email,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Verify name
|
||||
final profileSetting =
|
||||
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
|
||||
|
||||
expect(profileSetting.name, name);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 2));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('get user icon and name from server', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
email: email,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.account);
|
||||
|
||||
// Verify name
|
||||
final profileSetting =
|
||||
tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
|
||||
|
||||
expect(profileSetting.name, name);
|
||||
});
|
||||
}
|
||||
+27
-68
@@ -1,11 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -117,53 +116,6 @@ void main() {
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste bulleted list in numbered list', (tester) async {
|
||||
const inAppJson =
|
||||
'{"document":{"type":"page","children":[{"type":"bulleted_list","children":[{"type":"bulleted_list","data":{"delta":[{"insert":"World"}]}}],"data":{"delta":[{"insert":"Hello"}]}}]}}';
|
||||
|
||||
await tester.pasteContent(
|
||||
inAppJson: inAppJson,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
// Insert two numbered list nodes
|
||||
// 1. Parent One
|
||||
// 2.
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: NumberedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "One"},
|
||||
],
|
||||
},
|
||||
),
|
||||
Node(
|
||||
type: NumberedListBlockKeys.type,
|
||||
attributes: {'delta': []},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection.collapsed(Position(path: [1]));
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
(editorState) {
|
||||
final secondNode = editorState.getNodeAtPath([1]);
|
||||
expect(secondNode?.delta?.toPlainText(), 'Hello');
|
||||
expect(secondNode?.children.length, 1);
|
||||
|
||||
final childNode = secondNode?.children.first;
|
||||
expect(childNode?.delta?.toPlainText(), 'World');
|
||||
expect(childNode?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
@@ -258,33 +210,42 @@ void main() {
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
});
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'auto convert url to link preview block',
|
||||
(tester) async {
|
||||
(widgetTester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
await widgetTester.pasteContent(
|
||||
plainText: url,
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -295,7 +256,6 @@ extension on WidgetTester {
|
||||
Future<void> Function(EditorState editorState)? beforeTest,
|
||||
String? plainText,
|
||||
String? html,
|
||||
String? inAppJson,
|
||||
(String, Uint8List?)? image,
|
||||
}) async {
|
||||
await initializeAppFlowy();
|
||||
@@ -311,7 +271,6 @@ extension on WidgetTester {
|
||||
ClipboardServiceData(
|
||||
plainText: plainText,
|
||||
html: html,
|
||||
inAppJson: inAppJson,
|
||||
image: image,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -16,8 +16,6 @@ import 'document_with_image_block_test.dart' as document_with_image_block_test;
|
||||
import 'document_with_inline_math_equation_test.dart'
|
||||
as document_with_inline_math_equation_test;
|
||||
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
|
||||
import 'document_with_multi_image_block_test.dart'
|
||||
as document_with_multi_image_block_test;
|
||||
import 'document_with_outline_block_test.dart' as document_with_outline_block;
|
||||
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
|
||||
import 'edit_document_test.dart' as document_edit_test;
|
||||
@@ -40,7 +38,6 @@ void startTesting() {
|
||||
document_text_direction_test.main();
|
||||
document_option_action_test.main();
|
||||
document_with_image_block_test.main();
|
||||
document_with_multi_image_block_test.main();
|
||||
document_inline_page_reference_test.main();
|
||||
document_more_actions_test.main();
|
||||
}
|
||||
+8
-71
@@ -1,22 +1,21 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
hide UploadImageMenu, ResizableImage;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -37,7 +36,7 @@ void main() {
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
@@ -85,7 +84,7 @@ void main() {
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
@@ -138,7 +137,7 @@ void main() {
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
|
||||
name: LocaleKeys.document_plugins_image_addAnImage.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
@@ -162,67 +161,5 @@ void main() {
|
||||
expect(find.byType(UnsplashImageWidget), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('insert two images from local file at once', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Image');
|
||||
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(ImagePlaceholder), findsOneWidget);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: find.byType(ImagePlaceholder),
|
||||
matching: find.byType(AppFlowyPopover),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
final firstImage =
|
||||
await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final secondImage =
|
||||
await rootBundle.load('assets/test/images/sample.gif');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
|
||||
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final firstFile = File(firstImagePath)
|
||||
..writeAsBytesSync(firstImage.buffer.asUint8List());
|
||||
|
||||
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
|
||||
final secondFile = File(secondImagePath)
|
||||
..writeAsBytesSync(secondImage.buffer.asUint8List());
|
||||
|
||||
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
|
||||
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ResizableImage), findsNWidgets(2));
|
||||
|
||||
final firstNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(firstNode.type, ImageBlockKeys.type);
|
||||
expect(firstNode.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
|
||||
final secondNode =
|
||||
tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(secondNode.type, ImageBlockKeys.type);
|
||||
expect(secondNode.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
|
||||
// remove the temp files
|
||||
await Future.wait([firstFile.delete(), secondFile.delete()]);
|
||||
});
|
||||
});
|
||||
}
|
||||
-287
@@ -1,287 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
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/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_layouts.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
import '../board/board_hide_groups_test.dart';
|
||||
|
||||
void main() {
|
||||
setUp(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('multi image block in document', () {
|
||||
testWidgets('insert images from local and use interactive viewer',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'multi image block test',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Photo gallery');
|
||||
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byType(MultiImagePlaceholder));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
final firstImage =
|
||||
await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final secondImage =
|
||||
await rootBundle.load('assets/test/images/sample.gif');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
|
||||
final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final firstFile = File(firstImagePath)
|
||||
..writeAsBytesSync(firstImage.buffer.asUint8List());
|
||||
|
||||
final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
|
||||
final secondFile = File(secondImagePath)
|
||||
..writeAsBytesSync(secondImage.buffer.asUint8List());
|
||||
|
||||
mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
|
||||
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ImageBrowserLayout), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, MultiImageBlockKeys.type);
|
||||
|
||||
final data = MultiImageData.fromJson(
|
||||
node.attributes[MultiImageBlockKeys.images],
|
||||
);
|
||||
|
||||
expect(data.images.length, 2);
|
||||
|
||||
// Start using the interactive viewer to view the image(s)
|
||||
final imageFinder = find
|
||||
.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is Image &&
|
||||
w.image is FileImage &&
|
||||
(w.image as FileImage).file.path.endsWith('.jpeg'),
|
||||
)
|
||||
.first;
|
||||
await tester.tap(imageFinder);
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
await tester.tap(imageFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final ivFinder = find.byType(InteractiveImageViewer);
|
||||
expect(ivFinder, findsOneWidget);
|
||||
|
||||
// go to next image
|
||||
await tester.tap(find.byFlowySvg(FlowySvgs.arrow_right_s));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Expect image to end with .gif
|
||||
final gifImageFinder = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is Image &&
|
||||
w.image is FileImage &&
|
||||
(w.image as FileImage).file.path.endsWith('.gif'),
|
||||
);
|
||||
|
||||
gifImageFinder.evaluate();
|
||||
expect(gifImageFinder.found.length, 2);
|
||||
|
||||
// go to previous image
|
||||
await tester.tap(find.byFlowySvg(FlowySvgs.arrow_left_s));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
gifImageFinder.evaluate();
|
||||
expect(gifImageFinder.found.length, 1);
|
||||
|
||||
// remove the temp files
|
||||
await Future.wait([firstFile.delete(), secondFile.delete()]);
|
||||
});
|
||||
|
||||
testWidgets('insert and delete images from network', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'multi image block test',
|
||||
);
|
||||
|
||||
// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName('Photo gallery');
|
||||
expect(find.byType(MultiImageBlockComponent), findsOneWidget);
|
||||
expect(find.byType(MultiImagePlaceholder), findsOneWidget);
|
||||
|
||||
await tester.tap(find.byType(MultiImagePlaceholder));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(UploadImageMenu), findsOneWidget);
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
);
|
||||
|
||||
const url =
|
||||
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
url,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.text(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ImageBrowserLayout), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, MultiImageBlockKeys.type);
|
||||
|
||||
final data = MultiImageData.fromJson(
|
||||
node.attributes[MultiImageBlockKeys.images],
|
||||
);
|
||||
|
||||
expect(data.images.length, 1);
|
||||
|
||||
final imageFinder = find
|
||||
.byWidgetPredicate(
|
||||
(w) => w is FlowyNetworkImage && w.url == url,
|
||||
)
|
||||
.first;
|
||||
|
||||
// Insert two images from network
|
||||
for (int i = 0; i < 2; i++) {
|
||||
// Hover on the image to show the image toolbar
|
||||
await tester.hoverOnWidget(
|
||||
imageFinder,
|
||||
onHover: () async {
|
||||
// Click on the add
|
||||
final addFinder = find.descendant(
|
||||
of: find.byType(MultiImageMenu),
|
||||
matching: find.byFlowySvg(FlowySvgs.add_s),
|
||||
);
|
||||
|
||||
expect(addFinder, findsOneWidget);
|
||||
await tester.tap(addFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
);
|
||||
|
||||
await tester.enterText(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.byType(TextField),
|
||||
),
|
||||
url,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.text(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// There should be 4 images visible now, where 2 are thumbnails
|
||||
expect(find.byType(ThumbnailItem), findsNWidgets(3));
|
||||
|
||||
// And all three use ImageRender
|
||||
expect(find.byType(ImageRender), findsNWidgets(4));
|
||||
|
||||
// Hover on and delete the first thumbnail image
|
||||
await tester.hoverOnWidget(find.byType(ThumbnailItem).first);
|
||||
|
||||
final deleteFinder = find
|
||||
.descendant(
|
||||
of: find.byType(ThumbnailItem),
|
||||
matching: find.byFlowySvg(FlowySvgs.delete_s),
|
||||
)
|
||||
.first;
|
||||
|
||||
expect(deleteFinder, findsOneWidget);
|
||||
await tester.tap(deleteFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(ImageRender), findsNWidgets(3));
|
||||
|
||||
// Delete one from interactive viewer
|
||||
await tester.tap(imageFinder);
|
||||
await tester.pump(kDoubleTapMinTime);
|
||||
await tester.tap(imageFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final ivFinder = find.byType(InteractiveImageViewer);
|
||||
expect(ivFinder, findsOneWidget);
|
||||
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(InteractiveImageToolbar),
|
||||
matching: find.byFlowySvg(FlowySvgs.delete_s),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(InteractiveImageViewer), findsNothing);
|
||||
|
||||
// There should be 1 image and the thumbnail for said image still visible
|
||||
expect(find.byType(ImageRender), findsNWidgets(2));
|
||||
});
|
||||
});
|
||||
}
|
||||
+10
-10
@@ -1,5 +1,5 @@
|
||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -17,25 +17,25 @@ void main() {
|
||||
await tester.openSettingsPage(SettingsPage.notifications);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final toggleFinder = find.byType(Toggle).first;
|
||||
final switchFinder = find.byType(Switch).first;
|
||||
|
||||
// Defaults to enabled
|
||||
Toggle toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, true);
|
||||
Switch switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, true);
|
||||
|
||||
// Disable
|
||||
await tester.tap(toggleFinder);
|
||||
await tester.tap(switchFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, false);
|
||||
switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, false);
|
||||
|
||||
// Enable again
|
||||
await tester.tap(toggleFinder);
|
||||
await tester.tap(switchFinder);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
toggleWidget = tester.widget(toggleFinder);
|
||||
expect(toggleWidget.value, true);
|
||||
switchWidget = tester.widget(switchFinder);
|
||||
expect(switchWidget.value, true);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -8,10 +8,9 @@ void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Empty', () {
|
||||
testWidgets('empty test', (tester) async {
|
||||
testWidgets('toggle theme mode', (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([1])!.type,
|
||||
importedPageEditorState.getNodeAtPath([2])!.type,
|
||||
HeadingBlockKeys.type,
|
||||
);
|
||||
expect(
|
||||
importedPageEditorState.getNodeAtPath([2])!.type,
|
||||
importedPageEditorState.getNodeAtPath([4])!.type,
|
||||
TableBlockKeys.type,
|
||||
);
|
||||
});
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
|
||||
@@ -103,8 +103,8 @@ Future<AppFlowyEditor> setUpOpenAITesting(WidgetTester tester) async {
|
||||
}
|
||||
|
||||
Future<void> mockOpenAIRepository() async {
|
||||
await getIt.unregister<AIRepository>();
|
||||
getIt.registerFactoryAsync<AIRepository>(
|
||||
await getIt.unregister<OpenAIRepository>();
|
||||
getIt.registerFactoryAsync<OpenAIRepository>(
|
||||
() => Future.value(
|
||||
MockOpenAIRepository(),
|
||||
),
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/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(ShareButton);
|
||||
final shareButtonState = tester.widget(shareButton) as ShareButton;
|
||||
|
||||
final shareButton = find.byType(ShareActionList);
|
||||
final shareButtonState =
|
||||
tester.state(shareButton) as ShareActionListState;
|
||||
final path = await mockSaveFilePath(
|
||||
p.join(
|
||||
context.applicationDataDirectory,
|
||||
'${shareButtonState.view.name}.md',
|
||||
'${shareButtonState.name}.md',
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
|
||||
@@ -5,16 +7,13 @@ import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflow
|
||||
import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
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
|
||||
@@ -28,8 +27,8 @@ extension AppFlowyAuthTest on WidgetTester {
|
||||
|
||||
await tapButton(find.byType(SignInOutButton));
|
||||
|
||||
expectToSeeText(LocaleKeys.button_ok.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_ok.tr());
|
||||
expectToSeeText(LocaleKeys.button_confirm.tr());
|
||||
await tapButtonWithName(LocaleKeys.button_confirm.tr());
|
||||
}
|
||||
|
||||
Future<void> tapSignInAsGuest() async {
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
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/shared/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/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';
|
||||
@@ -30,10 +35,6 @@ 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';
|
||||
@@ -258,7 +259,7 @@ extension CommonOperations on WidgetTester {
|
||||
/// Tap the share button above the document page.
|
||||
Future<void> tapShareButton() async {
|
||||
final shareButton = find.byWidgetPredicate(
|
||||
(widget) => widget is ShareButton,
|
||||
(widget) => widget is DocumentShareButton,
|
||||
);
|
||||
await tapButton(shareButton);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/bl
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -171,17 +171,7 @@ class EditorOperations {
|
||||
///
|
||||
/// Must call [showSlashMenu] first.
|
||||
Future<void> tapSlashMenuItemWithName(String name) async {
|
||||
final slashMenu = find
|
||||
.ancestor(
|
||||
of: find.byType(SelectionMenuItemWidget),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is Scrollable,
|
||||
),
|
||||
)
|
||||
.first;
|
||||
final slashMenuItem = find.text(name, findRichText: true);
|
||||
await tester.scrollUntilVisible(slashMenuItem, 200, scrollable: slashMenu);
|
||||
// await tester.ensureVisible(slashMenuItem);
|
||||
await tester.tapButton(slashMenuItem);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ class MockOpenAIRepository extends HttpOpenAIRepository {
|
||||
required Future<void> Function() onStart,
|
||||
required Future<void> Function(TextCompletionResponse response) onProcess,
|
||||
required Future<void> Function() onEnd,
|
||||
required void Function(AIError error) onError,
|
||||
required void Function(OpenAIError error) onError,
|
||||
String? suffix,
|
||||
int maxTokens = 2048,
|
||||
double temperature = 0.3,
|
||||
|
||||
@@ -12,23 +12,17 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../desktop/board/board_hide_groups_test.dart';
|
||||
|
||||
import 'base.dart';
|
||||
import 'common_operations.dart';
|
||||
|
||||
extension AppFlowySettings on WidgetTester {
|
||||
/// Open settings page
|
||||
Future<void> openSettings() async {
|
||||
final settingsDialog = find.byType(SettingsDialog);
|
||||
// tap empty area to close the settings page
|
||||
while (settingsDialog.evaluate().isNotEmpty) {
|
||||
await tapAt(Offset.zero);
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
final settingsButton = find.byType(UserSettingButton);
|
||||
expect(settingsButton, findsOneWidget);
|
||||
await tapButton(settingsButton);
|
||||
|
||||
final settingsDialog = find.byType(SettingsDialog);
|
||||
expect(settingsDialog, findsOneWidget);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -65,8 +65,6 @@ 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)
|
||||
@@ -103,7 +101,6 @@ 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`)
|
||||
@@ -152,8 +149,6 @@ 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:
|
||||
@@ -175,7 +170,7 @@ SPEC CHECKSUMS:
|
||||
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
|
||||
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
|
||||
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
|
||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||
fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
|
||||
image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
|
||||
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
|
||||
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
|
||||
@@ -184,7 +179,6 @@ SPEC CHECKSUMS:
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
|
||||
|
||||
@@ -18,13 +18,6 @@ class KVKeys {
|
||||
/// {'dx': 10.0, 'dy': 10.0}
|
||||
static const String windowPosition = 'windowPosition';
|
||||
|
||||
/// The key for saving the window status
|
||||
///
|
||||
/// The value is a json string with the following format:
|
||||
/// { 'windowMaximized': true }
|
||||
///
|
||||
static const String windowMaximized = 'windowMaximized';
|
||||
|
||||
static const String kDocumentAppearanceFontSize =
|
||||
'kDocumentAppearanceFontSize';
|
||||
static const String kDocumentAppearanceFontFamily =
|
||||
|
||||
@@ -3,9 +3,10 @@ import 'dart:io';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/window_title_bar.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.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';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -102,38 +103,34 @@ class MoveWindowDetectorState extends State<MoveWindowDetector> {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
final color = Theme.of(context).isLightMode ? Colors.white : Colors.black;
|
||||
final textSpan = TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: '${LocaleKeys.sideBar_openSidebar.tr()}\n',
|
||||
style: context.tooltipTextStyle(),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: color),
|
||||
),
|
||||
TextSpan(
|
||||
text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\',
|
||||
style: context
|
||||
.tooltipTextStyle()
|
||||
?.copyWith(color: Theme.of(context).hintColor),
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(color: Theme.of(context).hintColor),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
return FlowyTooltip(
|
||||
richMessage: textSpan,
|
||||
child: Listener(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onPointerDown: (_) => context
|
||||
child: FlowyIconButton(
|
||||
hoverColor: Colors.transparent,
|
||||
onPressed: () => context
|
||||
.read<HomeSettingBloc>()
|
||||
.add(const HomeSettingEvent.collapseMenu()),
|
||||
child: FlowyHover(
|
||||
child: Container(
|
||||
width: 24,
|
||||
padding: const EdgeInsets.all(4),
|
||||
child: const RotatedBox(
|
||||
quarterTurns: 2,
|
||||
child: FlowySvg(FlowySvgs.hide_menu_s),
|
||||
),
|
||||
),
|
||||
),
|
||||
iconPadding: const EdgeInsets.all(4.0),
|
||||
icon: context.read<HomeSettingBloc>().state.isMenuCollapsed
|
||||
? const FlowySvg(FlowySvgs.show_menu_s)
|
||||
: const FlowySvg(FlowySvgs.hide_menu_m),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
-7
@@ -29,11 +29,4 @@ abstract class Env {
|
||||
defaultValue: '',
|
||||
)
|
||||
static const String afCloudUrl = _Env.afCloudUrl;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: false,
|
||||
varName: 'INTERNAL_BUILD',
|
||||
defaultValue: '',
|
||||
)
|
||||
static const String internalBuild = _Env.internalBuild;
|
||||
}
|
||||
-3
@@ -23,9 +23,6 @@ class DocumentPageStyleBloc
|
||||
await event.when(
|
||||
initial: () async {
|
||||
try {
|
||||
if (view.id.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final layoutObject =
|
||||
await ViewBackendService.getView(view.id).fold(
|
||||
(s) => jsonDecode(s.extra),
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
import 'package:appflowy/shared/feedback_gesture_detector.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnimatedGestureDetector extends StatefulWidget {
|
||||
const AnimatedGestureDetector({
|
||||
super.key,
|
||||
this.scaleFactor = 0.98,
|
||||
this.feedback = true,
|
||||
this.duration = const Duration(milliseconds: 100),
|
||||
this.alignment = Alignment.center,
|
||||
this.behavior = HitTestBehavior.opaque,
|
||||
required this.onTapUp,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
final Widget child;
|
||||
final double scaleFactor;
|
||||
final Duration duration;
|
||||
final Alignment alignment;
|
||||
final bool feedback;
|
||||
final HitTestBehavior behavior;
|
||||
final VoidCallback onTapUp;
|
||||
|
||||
@override
|
||||
State<AnimatedGestureDetector> createState() =>
|
||||
_AnimatedGestureDetectorState();
|
||||
}
|
||||
|
||||
class _AnimatedGestureDetectorState extends State<AnimatedGestureDetector> {
|
||||
double scale = 1.0;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
behavior: widget.behavior,
|
||||
onTapUp: (details) {
|
||||
setState(() => scale = 1.0);
|
||||
|
||||
HapticFeedbackType.vibrate.call();
|
||||
|
||||
widget.onTapUp();
|
||||
},
|
||||
onTapDown: (details) {
|
||||
setState(() => scale = widget.scaleFactor);
|
||||
},
|
||||
child: AnimatedScale(
|
||||
scale: scale,
|
||||
alignment: widget.alignment,
|
||||
duration: widget.duration,
|
||||
child: widget.child,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -125,7 +125,7 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
return child;
|
||||
},
|
||||
)
|
||||
: SafeArea(child: child);
|
||||
: child;
|
||||
return Scaffold(
|
||||
extendBodyBehindAppBar: isDocument,
|
||||
appBar: appBar,
|
||||
|
||||
+5
-10
@@ -24,10 +24,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
|
||||
height: 52.0,
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.icon_document_s,
|
||||
size: Size.square(20),
|
||||
size: Size.square(18),
|
||||
),
|
||||
showTopBorder: false,
|
||||
showBottomBorder: false,
|
||||
onTap: () => onAction(ViewLayoutPB.Document),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
@@ -35,10 +34,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
|
||||
height: 52.0,
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.icon_grid_s,
|
||||
size: Size.square(20),
|
||||
size: Size.square(18),
|
||||
),
|
||||
showTopBorder: false,
|
||||
showBottomBorder: false,
|
||||
onTap: () => onAction(ViewLayoutPB.Grid),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
@@ -46,10 +44,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
|
||||
height: 52.0,
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.icon_board_s,
|
||||
size: Size.square(20),
|
||||
size: Size.square(18),
|
||||
),
|
||||
showTopBorder: false,
|
||||
showBottomBorder: false,
|
||||
onTap: () => onAction(ViewLayoutPB.Board),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
@@ -57,10 +54,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
|
||||
height: 52.0,
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.icon_calendar_s,
|
||||
size: Size.square(20),
|
||||
size: Size.square(18),
|
||||
),
|
||||
showTopBorder: false,
|
||||
showBottomBorder: false,
|
||||
onTap: () => onAction(ViewLayoutPB.Calendar),
|
||||
),
|
||||
FlowyOptionTile.text(
|
||||
@@ -68,10 +64,9 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
|
||||
height: 52.0,
|
||||
leftIcon: const FlowySvg(
|
||||
FlowySvgs.chat_ai_page_s,
|
||||
size: Size.square(20),
|
||||
size: Size.square(18),
|
||||
),
|
||||
showTopBorder: false,
|
||||
showBottomBorder: false,
|
||||
onTap: () => onAction(ViewLayoutPB.Chat),
|
||||
),
|
||||
],
|
||||
|
||||
+41
-7
@@ -1,3 +1,4 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
|
||||
@@ -5,7 +6,6 @@ import 'package:appflowy/startup/tasks/app_widget.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -109,6 +109,16 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
||||
await _showConfirmDialog(
|
||||
onDelete: () {
|
||||
recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId]));
|
||||
|
||||
fToast.showToast(
|
||||
child: const _RemoveToast(),
|
||||
positionedToastBuilder: (context, child) {
|
||||
return Positioned.fill(
|
||||
top: 450,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -126,14 +136,38 @@ class _MobileViewItemBottomSheetState extends State<MobileViewItemBottomSheet> {
|
||||
),
|
||||
onRightButtonPressed: (context) {
|
||||
onDelete();
|
||||
|
||||
Navigator.pop(context);
|
||||
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.sideBar_removeSuccess.tr(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RemoveToast extends StatelessWidget {
|
||||
const _RemoveToast();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
color: const Color(0xE5171717),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const FlowySvg(
|
||||
FlowySvgs.success_s,
|
||||
blendMode: null,
|
||||
),
|
||||
const HSpace(8.0),
|
||||
FlowyText.regular(
|
||||
LocaleKeys.sideBar_removeSuccess.tr(),
|
||||
fontSize: 16.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+11
-25
@@ -7,7 +7,6 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.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_ui/flowy_infra_ui.dart';
|
||||
@@ -41,32 +40,18 @@ enum MobilePaneActionType {
|
||||
backgroundColor: const Color(0xFFFA217F),
|
||||
svg: FlowySvgs.favorite_section_remove_from_favorite_s,
|
||||
size: 24.0,
|
||||
onPressed: (context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
|
||||
);
|
||||
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view));
|
||||
},
|
||||
onPressed: (context) => context
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)),
|
||||
);
|
||||
case MobilePaneActionType.addToFavorites:
|
||||
return MobileSlideActionButton(
|
||||
backgroundColor: const Color(0xFF00C8FF),
|
||||
svg: FlowySvgs.favorite_s,
|
||||
size: 24.0,
|
||||
onPressed: (context) {
|
||||
showToastNotification(
|
||||
context,
|
||||
message: LocaleKeys.button_favoriteSuccessfully.tr(),
|
||||
);
|
||||
|
||||
context
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view));
|
||||
},
|
||||
onPressed: (context) => context
|
||||
.read<FavoriteBloc>()
|
||||
.add(FavoriteEvent.toggle(context.read<ViewBloc>().view)),
|
||||
);
|
||||
case MobilePaneActionType.add:
|
||||
return MobileSlideActionButton(
|
||||
@@ -84,7 +69,6 @@ enum MobilePaneActionType {
|
||||
showDragHandle: true,
|
||||
showCloseButton: true,
|
||||
useRootNavigator: true,
|
||||
showDivider: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (sheetContext) {
|
||||
return AddNewPageWidgetBottomSheet(
|
||||
@@ -161,6 +145,8 @@ enum MobilePaneActionType {
|
||||
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
|
||||
: MobileViewItemBottomSheetBodyAction.addToFavorites,
|
||||
MobileViewItemBottomSheetBodyAction.divider,
|
||||
if (view.layout != ViewLayoutPB.Chat)
|
||||
MobileViewItemBottomSheetBodyAction.duplicate,
|
||||
MobileViewItemBottomSheetBodyAction.divider,
|
||||
MobileViewItemBottomSheetBodyAction.removeFromRecent,
|
||||
];
|
||||
@@ -170,6 +156,7 @@ enum MobilePaneActionType {
|
||||
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
|
||||
: MobileViewItemBottomSheetBodyAction.addToFavorites,
|
||||
MobileViewItemBottomSheetBodyAction.divider,
|
||||
MobileViewItemBottomSheetBodyAction.duplicate,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -194,13 +181,12 @@ ActionPane buildEndActionPane(
|
||||
bool needSpace = true,
|
||||
MobilePageCardType? cardType,
|
||||
FolderSpaceType? spaceType,
|
||||
required double spaceRatio,
|
||||
}) {
|
||||
return ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
extentRatio: actions.length / spaceRatio,
|
||||
extentRatio: actions.length / 5,
|
||||
children: [
|
||||
if (needSpace) const HSpace(60),
|
||||
if (needSpace) const HSpace(20),
|
||||
...actions.map(
|
||||
(action) => action.actionButton(
|
||||
context,
|
||||
|
||||
+4
-9
@@ -70,7 +70,6 @@ Future<T?> showMobileBottomSheet<T>(
|
||||
backgroundColor ??= Theme.of(context).brightness == Brightness.light
|
||||
? const Color(0xFFF7F8FB)
|
||||
: const Color(0xFF23262B);
|
||||
barrierColor ??= Colors.black.withOpacity(0.3);
|
||||
|
||||
return showModalBottomSheet<T>(
|
||||
context: context,
|
||||
@@ -227,14 +226,10 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
Align(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 250),
|
||||
child: FlowyText(
|
||||
title,
|
||||
fontSize: 17.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
child: FlowyText(
|
||||
title,
|
||||
fontSize: 16.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (showDoneButton)
|
||||
|
||||
+2
@@ -100,6 +100,8 @@ class _FavoriteViews extends StatelessWidget {
|
||||
child: ListView.separated(
|
||||
key: const PageStorageKey('favorite_views_page_storage_key'),
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
|
||||
-1
@@ -72,7 +72,6 @@ class MobileFavoriteFolder extends StatelessWidget {
|
||||
MobilePaneActionType.more,
|
||||
],
|
||||
spaceType: FolderSpaceType.favorite,
|
||||
spaceRatio: 5,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -29,6 +29,8 @@ class _MobileHomeSpaceState extends State<MobileHomeSpace>
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
top: HomeSpaceViewSizes.mVerticalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -92,12 +91,7 @@ class MobileFolders extends StatelessWidget {
|
||||
children: [
|
||||
..._buildSpaceOrSection(context, state),
|
||||
const VSpace(4.0),
|
||||
const Padding(
|
||||
padding: EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
),
|
||||
child: _TrashButton(),
|
||||
),
|
||||
const _TrashButton(),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/auth/auth_service.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
@@ -18,6 +15,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
@@ -114,9 +112,6 @@ class _MobileHomePageState extends State<MobileHomePage> {
|
||||
create: (context) =>
|
||||
FavoriteBloc()..add(const FavoriteEvent.initial()),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: getIt<ReminderBloc>()..add(const ReminderEvent.started()),
|
||||
),
|
||||
],
|
||||
child: BlocConsumer<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
buildWhen: (previous, current) =>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/gesture.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
|
||||
@@ -114,9 +113,8 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
if (currentWorkspace == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return AnimatedGestureDetector(
|
||||
alignment: Alignment.centerLeft,
|
||||
onTapUp: () {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
context.read<UserWorkspaceBloc>().add(
|
||||
const UserWorkspaceEvent.fetchWorkspaces(),
|
||||
);
|
||||
@@ -145,7 +143,7 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
: const HSpace(8),
|
||||
FlowyText.semibold(
|
||||
currentWorkspace.name,
|
||||
fontSize: 20.0,
|
||||
fontSize: 16.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
@@ -164,10 +162,9 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
showHeader: true,
|
||||
showDragHandle: true,
|
||||
showCloseButton: true,
|
||||
useRootNavigator: true,
|
||||
title: LocaleKeys.workspace_menuTitle.tr(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (sheetContext) {
|
||||
builder: (_) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<UserWorkspaceBloc>(),
|
||||
child: BlocBuilder<UserWorkspaceBloc, UserWorkspaceState>(
|
||||
@@ -182,7 +179,7 @@ class _MobileWorkspace extends StatelessWidget {
|
||||
currentWorkspace: currentWorkspace,
|
||||
workspaces: workspaces,
|
||||
onWorkspaceSelected: (workspace) {
|
||||
Navigator.of(sheetContext).pop();
|
||||
context.pop();
|
||||
|
||||
if (workspace == currentWorkspace) {
|
||||
return;
|
||||
|
||||
+2
-1
@@ -38,7 +38,8 @@ class _MobileRecentFolderState extends State<MobileRecentFolder> {
|
||||
builder: (context, state) {
|
||||
final ids = <String>{};
|
||||
|
||||
List<ViewPB> recentViews = state.views.map((e) => e.item).toList();
|
||||
List<ViewPB> recentViews =
|
||||
state.views.reversed.map((e) => e.item).toList();
|
||||
recentViews.retainWhere((element) => ids.add(element.id));
|
||||
|
||||
// only keep the first 20 items.
|
||||
|
||||
+3
-1
@@ -49,7 +49,7 @@ class _MobileRecentSpaceState extends State<MobileRecentSpace>
|
||||
|
||||
List<SectionViewPB> _filterRecentViews(List<SectionViewPB> recentViews) {
|
||||
final ids = <String>{};
|
||||
final filteredRecentViews = recentViews.toList();
|
||||
final filteredRecentViews = recentViews.reversed.toList();
|
||||
filteredRecentViews.retainWhere((e) => ids.add(e.item.id));
|
||||
return filteredRecentViews;
|
||||
}
|
||||
@@ -72,6 +72,8 @@ class _RecentViews extends StatelessWidget {
|
||||
child: ListView.separated(
|
||||
key: const PageStorageKey('recent_views_page_storage_key'),
|
||||
padding: EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
right: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
bottom: HomeSpaceViewSizes.mVerticalPadding +
|
||||
MediaQuery.of(context).padding.bottom,
|
||||
),
|
||||
|
||||
-1
@@ -82,7 +82,6 @@ class MobileSectionFolder extends StatelessWidget {
|
||||
],
|
||||
spaceType: spaceType,
|
||||
needSpace: false,
|
||||
spaceRatio: 5,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,7 +5,6 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/gesture.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||
@@ -16,7 +15,6 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
|
||||
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -78,14 +76,13 @@ class MobileViewPage extends StatelessWidget {
|
||||
: MobilePaneActionType.addToFavorites,
|
||||
],
|
||||
cardType: type,
|
||||
spaceRatio: 4,
|
||||
),
|
||||
child: AnimatedGestureDetector(
|
||||
onTapUp: () => context.pushView(view),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTapUp: (_) => context.pushView(view),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
|
||||
Expanded(child: _buildDescription(context, state)),
|
||||
const HSpace(20.0),
|
||||
SizedBox(
|
||||
@@ -93,7 +90,6 @@ class MobileViewPage extends StatelessWidget {
|
||||
height: 60,
|
||||
child: _buildCover(context, state),
|
||||
),
|
||||
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -215,7 +211,7 @@ class MobileViewPage extends StatelessWidget {
|
||||
|
||||
Widget _buildLastViewed(BuildContext context) {
|
||||
final textColor = Theme.of(context).isLightMode
|
||||
? const Color(0x7F171717)
|
||||
? const Color(0xFF171717)
|
||||
: Colors.white.withOpacity(0.45);
|
||||
if (timestamp == null) {
|
||||
return const SizedBox.shrink();
|
||||
|
||||
@@ -50,17 +50,22 @@ class _MobileSpaceState extends State<MobileSpace> {
|
||||
MobileSpaceHeader(
|
||||
isExpanded: state.isExpanded,
|
||||
space: currentSpace,
|
||||
onAdded: () => _showCreatePageMenu(currentSpace),
|
||||
onAdded: () {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
index: 0,
|
||||
),
|
||||
);
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.expand(currentSpace, true),
|
||||
);
|
||||
},
|
||||
onPressed: () => _showSpaceMenu(context),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
left: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
),
|
||||
child: _Pages(
|
||||
key: ValueKey(currentSpace.id),
|
||||
space: currentSpace,
|
||||
),
|
||||
_Pages(
|
||||
key: ValueKey(currentSpace.id),
|
||||
space: currentSpace,
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -76,7 +81,6 @@ class _MobileSpaceState extends State<MobileSpace> {
|
||||
showDragHandle: true,
|
||||
showCloseButton: true,
|
||||
showDoneButton: true,
|
||||
useRootNavigator: true,
|
||||
title: LocaleKeys.space_title.tr(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (_) {
|
||||
@@ -95,42 +99,9 @@ class _MobileSpaceState extends State<MobileSpace> {
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
layout: ViewLayoutPB.Document,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showCreatePageMenu(ViewPB space) {
|
||||
final title = space.name;
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: title,
|
||||
showDragHandle: true,
|
||||
showCloseButton: true,
|
||||
useRootNavigator: true,
|
||||
showDivider: false,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (sheetContext) {
|
||||
return AddNewPageWidgetBottomSheet(
|
||||
view: space,
|
||||
onAction: (layout) {
|
||||
Navigator.of(sheetContext).pop();
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.createPage(
|
||||
name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
|
||||
layout: layout,
|
||||
index: 0,
|
||||
),
|
||||
);
|
||||
context.read<SpaceBloc>().add(
|
||||
SpaceEvent.expand(space, true),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Pages extends StatelessWidget {
|
||||
@@ -175,7 +146,7 @@ class _Pages extends StatelessWidget {
|
||||
MobilePaneActionType.add,
|
||||
],
|
||||
spaceType: spaceType,
|
||||
spaceRatio: 4,
|
||||
needSpace: false,
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
+2
-11
@@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -31,7 +30,6 @@ class MobileSpaceHeader extends StatelessWidget {
|
||||
height: 48,
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
|
||||
SpaceIcon(
|
||||
dimension: 24,
|
||||
space: space,
|
||||
@@ -51,15 +49,8 @@ class MobileSpaceHeader extends StatelessWidget {
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: onAdded,
|
||||
child: Container(
|
||||
// expand the touch area
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: HomeSpaceViewSizes.mHorizontalPadding,
|
||||
vertical: 8.0,
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.m_space_add_s,
|
||||
),
|
||||
child: const FlowySvg(
|
||||
FlowySvgs.m_space_add_s,
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -59,7 +59,6 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
|
||||
children: [
|
||||
FlowyText.medium(
|
||||
space.name,
|
||||
fontSize: 16.0,
|
||||
),
|
||||
const HSpace(6.0),
|
||||
if (space.spacePermission == SpacePermission.private)
|
||||
@@ -69,17 +68,16 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
margin: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
iconPadding: 10,
|
||||
leftIcon: SpaceIcon(
|
||||
dimension: 24,
|
||||
space: space,
|
||||
cornerRadius: 6.0,
|
||||
),
|
||||
leftIconSize: const Size.square(24),
|
||||
leftIconSize: const Size.square(20),
|
||||
rightIcon: isSelected
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_blue_check_s,
|
||||
FlowySvgs.workspace_selected_s,
|
||||
blendMode: null,
|
||||
)
|
||||
: null,
|
||||
|
||||
+10
-13
@@ -138,20 +138,17 @@ class _WorkspaceMenuItem extends StatelessWidget {
|
||||
height: 60,
|
||||
showTopBorder: showTopBorder,
|
||||
showBottomBorder: false,
|
||||
leftIcon: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4.0),
|
||||
child: WorkspaceIcon(
|
||||
enableEdit: false,
|
||||
iconSize: 26,
|
||||
fontSize: 16.0,
|
||||
workspace: workspace,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
workspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
leftIcon: WorkspaceIcon(
|
||||
enableEdit: false,
|
||||
iconSize: 26,
|
||||
fontSize: 16.0,
|
||||
workspace: workspace,
|
||||
onSelected: (result) => context.read<UserWorkspaceBloc>().add(
|
||||
UserWorkspaceEvent.updateWorkspaceIcon(
|
||||
workspace.workspaceId,
|
||||
result.emoji,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
trailing: workspace.workspaceId == currentWorkspace.workspaceId
|
||||
? const FlowySvg(
|
||||
|
||||
@@ -50,9 +50,6 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
final backgroundColor = isLightMode
|
||||
? Colors.white.withOpacity(0.95)
|
||||
: const Color(0xFF23262B).withOpacity(0.95);
|
||||
final borderColor = isLightMode
|
||||
? const Color(0x141F2329)
|
||||
: const Color(0xFF23262B).withOpacity(0.5);
|
||||
return Scaffold(
|
||||
body: navigationShell,
|
||||
extendBody: true,
|
||||
@@ -65,7 +62,9 @@ class MobileBottomNavigationBar extends StatelessWidget {
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: isLightMode
|
||||
? Border(top: BorderSide(color: borderColor))
|
||||
? Border(
|
||||
top: BorderSide(color: Theme.of(context).dividerColor),
|
||||
)
|
||||
: null,
|
||||
color: backgroundColor,
|
||||
),
|
||||
|
||||
@@ -300,7 +300,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
)
|
||||
: Opacity(
|
||||
opacity: 0.7,
|
||||
child: widget.view.defaultIcon(size: const Size.square(18)),
|
||||
child: widget.view.defaultIcon(),
|
||||
);
|
||||
return SizedBox(width: 18.0, child: icon);
|
||||
}
|
||||
|
||||
+2
-2
@@ -25,14 +25,14 @@ class AboutSettingGroup extends StatelessWidget {
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/privacy'),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/privacy/app'),
|
||||
),
|
||||
MobileSettingItem(
|
||||
name: LocaleKeys.settings_mobile_termsAndConditions.tr(),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/terms'),
|
||||
onTap: () => afLaunchUrlString('https://appflowy.io/terms/app'),
|
||||
),
|
||||
if (kDebugMode)
|
||||
MobileSettingItem(
|
||||
|
||||
+29
-46
@@ -17,23 +17,17 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
required this.questionId,
|
||||
}) : super(ChatAIMessageState.initial(message)) {
|
||||
if (state.stream != null) {
|
||||
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());
|
||||
}
|
||||
},
|
||||
);
|
||||
_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)));
|
||||
}
|
||||
});
|
||||
|
||||
if (state.stream!.error != null) {
|
||||
Future.delayed(const Duration(milliseconds: 300), () {
|
||||
@@ -48,16 +42,11 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
initial: () async {},
|
||||
updateText: (newText) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
text: newText,
|
||||
messageState: const MessageState.ready(),
|
||||
),
|
||||
);
|
||||
newText: (newText) {
|
||||
emit(state.copyWith(text: state.text + newText, error: null));
|
||||
},
|
||||
receiveError: (error) {
|
||||
emit(state.copyWith(messageState: MessageState.onError(error)));
|
||||
emit(state.copyWith(error: error));
|
||||
},
|
||||
retry: () {
|
||||
if (questionId is! Int64) {
|
||||
@@ -66,7 +55,8 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageState: const MessageState.loading(),
|
||||
retryState: const LoadingState.loading(),
|
||||
error: null,
|
||||
),
|
||||
);
|
||||
|
||||
@@ -92,14 +82,8 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
text: text,
|
||||
messageState: const MessageState.ready(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAIResponseLimit: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageState: const MessageState.onAIResponseLimit(),
|
||||
error: null,
|
||||
retryState: const LoadingState.finish(),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -108,6 +92,13 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_subscription?.cancel();
|
||||
return super.close();
|
||||
}
|
||||
|
||||
StreamSubscription<AnswerStreamElement>? _subscription;
|
||||
final String chatId;
|
||||
final Int64? questionId;
|
||||
}
|
||||
@@ -115,34 +106,26 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
@freezed
|
||||
class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
||||
const factory ChatAIMessageEvent.initial() = Initial;
|
||||
const factory ChatAIMessageEvent.updateText(String text) = _UpdateText;
|
||||
const factory ChatAIMessageEvent.newText(String text) = _NewText;
|
||||
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 MessageState messageState,
|
||||
required LoadingState retryState,
|
||||
}) = _ChatAIMessageState;
|
||||
|
||||
factory ChatAIMessageState.initial(dynamic text) {
|
||||
return ChatAIMessageState(
|
||||
text: text is String ? text : "",
|
||||
stream: text is AnswerStream ? text : null,
|
||||
messageState: const MessageState.ready(),
|
||||
retryState: const LoadingState.finish(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@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,7 +7,6 @@ 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';
|
||||
@@ -120,6 +119,7 @@ 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,8 +380,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
}
|
||||
|
||||
Message _createStreamMessage(AnswerStream stream, Int64 questionMessageId) {
|
||||
final streamMessageId = (questionMessageId + 1).toString();
|
||||
|
||||
final streamMessageId = nanoid();
|
||||
lastStreamMessageId = streamMessageId;
|
||||
|
||||
return TextMessage(
|
||||
@@ -481,7 +480,7 @@ class ChatState with _$ChatState {
|
||||
@freezed
|
||||
class LoadingState with _$LoadingState {
|
||||
const factory LoadingState.loading() = _Loading;
|
||||
const factory LoadingState.finish({FlowyError? error}) = _Finish;
|
||||
const factory LoadingState.finish() = _Finish;
|
||||
}
|
||||
|
||||
enum OnetimeShotType {
|
||||
@@ -525,6 +524,8 @@ OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
|
||||
return null;
|
||||
}
|
||||
|
||||
typedef AnswerStreamElement = String;
|
||||
|
||||
class AnswerStream {
|
||||
AnswerStream() {
|
||||
_port.handler = _controller.add;
|
||||
@@ -532,53 +533,23 @@ 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<String> _controller = StreamController.broadcast();
|
||||
late StreamSubscription<String> _subscription;
|
||||
final StreamController<AnswerStreamElement> _controller =
|
||||
StreamController.broadcast();
|
||||
late StreamSubscription<AnswerStreamElement> _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();
|
||||
@@ -586,21 +557,9 @@ class AnswerStream {
|
||||
_port.close();
|
||||
}
|
||||
|
||||
void 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!();
|
||||
}
|
||||
StreamSubscription<AnswerStreamElement> listen(
|
||||
void Function(AnswerStreamElement event)? onData,
|
||||
) {
|
||||
return _controller.stream.listen(onData);
|
||||
}
|
||||
}
|
||||
@@ -1,139 +0,0 @@
|
||||
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: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(
|
||||
stateCallback: (pluginState) {
|
||||
if (!isClosed) {
|
||||
add(ChatFileEvent.updatePluginState(pluginState));
|
||||
}
|
||||
},
|
||||
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, String fileName) async {
|
||||
emit(
|
||||
state.copyWith(
|
||||
indexFileIndicator: IndexFileIndicator.indexing(fileName),
|
||||
),
|
||||
);
|
||||
final payload = ChatFilePB(filePath: filePath, chatId: chatId);
|
||||
unawaited(
|
||||
ChatEventChatWithFile(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold((_) {
|
||||
add(
|
||||
ChatFileEvent.updateIndexFile(
|
||||
IndexFileIndicator.finish(fileName),
|
||||
),
|
||||
);
|
||||
}, (err) {
|
||||
add(
|
||||
ChatFileEvent.updateIndexFile(
|
||||
IndexFileIndicator.error(err.msg),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
},
|
||||
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,
|
||||
chatState: chatState,
|
||||
),
|
||||
);
|
||||
},
|
||||
updateIndexFile: (IndexFileIndicator indicator) {
|
||||
emit(
|
||||
state.copyWith(indexFileIndicator: indicator),
|
||||
);
|
||||
},
|
||||
updatePluginState: (LocalAIPluginStatePB chatState) {
|
||||
final fileEnabled = state.chatState?.fileEnabled ?? false;
|
||||
final supportChatWithFile =
|
||||
fileEnabled && chatState.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, String fileName) =
|
||||
_NewFile;
|
||||
const factory ChatFileEvent.updateChatState(LocalAIChatPB chatState) =
|
||||
_UpdateChatState;
|
||||
const factory ChatFileEvent.updatePluginState(
|
||||
LocalAIPluginStatePB chatState,
|
||||
) = _UpdatePluginState;
|
||||
const factory ChatFileEvent.updateIndexFile(IndexFileIndicator indicator) =
|
||||
_UpdateIndexFile;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatFileState with _$ChatFileState {
|
||||
const factory ChatFileState({
|
||||
@Default(false) bool supportChatWithFile,
|
||||
IndexFileIndicator? indexFileIndicator,
|
||||
LocalAIChatPB? chatState,
|
||||
}) = _ChatFileState;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class IndexFileIndicator with _$IndexFileIndicator {
|
||||
const factory IndexFileIndicator.finish(String fileName) = _Finish;
|
||||
const factory IndexFileIndicator.indexing(String fileName) = _Indexing;
|
||||
const factory IndexFileIndicator.error(String error) = _Error;
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
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(
|
||||
chatStateCallback: (aiState) {
|
||||
if (!isClosed) {
|
||||
add(ChatInputEvent.updateState(aiState));
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
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 ChatEventGetLocalAIChatState().send();
|
||||
result.fold(
|
||||
(aiState) {
|
||||
if (!isClosed) {
|
||||
add(
|
||||
ChatInputEvent.updateState(aiState),
|
||||
);
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
Log.error(err.toString());
|
||||
},
|
||||
);
|
||||
},
|
||||
updateState: (aiState) {
|
||||
if (aiState.enabled) {
|
||||
emit(const ChatInputState(aiType: _LocalAI()));
|
||||
} else {
|
||||
emit(const ChatInputState(aiType: _AppFlowyAI()));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatInputEvent with _$ChatInputEvent {
|
||||
const factory ChatInputEvent.started() = _Started;
|
||||
const factory ChatInputEvent.updateState(LocalAIChatPB aiState) =
|
||||
_UpdateAIState;
|
||||
}
|
||||
|
||||
@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;
|
||||
}
|
||||
@@ -65,12 +65,6 @@ class AIChatPagePlugin extends Plugin {
|
||||
_viewInfoBloc = ViewInfoBloc(view: notifier.view)
|
||||
..add(const ViewInfoEvent.started());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_viewInfoBloc.close();
|
||||
notifier.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
@@ -95,7 +89,6 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
final deletedView = notifier.isDeleted.value;
|
||||
@@ -118,7 +111,4 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
|
||||
@override
|
||||
List<NavigationItem> get navigationItems => [this];
|
||||
|
||||
@override
|
||||
EdgeInsets get contentPadding => EdgeInsets.zero;
|
||||
}
|
||||
@@ -1,11 +1,3 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_file_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/ai_message_bubble.dart';
|
||||
@@ -18,12 +10,14 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart' show Chat;
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
|
||||
import 'presentation/chat_input/chat_input.dart';
|
||||
import 'presentation/chat_input.dart';
|
||||
import 'presentation/chat_popmenu.dart';
|
||||
import 'presentation/chat_theme.dart';
|
||||
import 'presentation/chat_user_invalid_message.dart';
|
||||
@@ -33,11 +27,11 @@ import 'presentation/message/user_text_message.dart';
|
||||
|
||||
class AIChatUILayout {
|
||||
static EdgeInsets get chatPadding =>
|
||||
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 20);
|
||||
isMobile ? EdgeInsets.zero : const EdgeInsets.symmetric(horizontal: 70);
|
||||
|
||||
static EdgeInsets get welcomePagePadding => isMobile
|
||||
? const EdgeInsets.symmetric(horizontal: 20)
|
||||
: const EdgeInsets.symmetric(horizontal: 50);
|
||||
: const EdgeInsets.symmetric(horizontal: 100);
|
||||
|
||||
static double get messageWidthRatio => 0.85;
|
||||
|
||||
@@ -50,12 +44,11 @@ class AIChatUILayout {
|
||||
query.padding.right,
|
||||
query.viewInsets.bottom + query.padding.bottom,
|
||||
)
|
||||
: const EdgeInsets.symmetric(horizontal: 50) +
|
||||
const EdgeInsets.only(bottom: 20);
|
||||
: const EdgeInsets.symmetric(horizontal: 70);
|
||||
}
|
||||
}
|
||||
|
||||
class AIChatPage extends StatelessWidget {
|
||||
class AIChatPage extends StatefulWidget {
|
||||
const AIChatPage({
|
||||
super.key,
|
||||
required this.view,
|
||||
@@ -68,114 +61,10 @@ class AIChatPage extends StatelessWidget {
|
||||
final UserProfilePB userProfile;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider(
|
||||
create: (_) => ChatFileBloc(chatId: view.id.toString())
|
||||
..add(const ChatFileEvent.initial()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => ChatBloc(
|
||||
view: view,
|
||||
userProfile: userProfile,
|
||||
)..add(const ChatEvent.initialLoad()),
|
||||
),
|
||||
BlocProvider(
|
||||
create: (_) => ChatInputBloc()..add(const ChatInputEvent.started()),
|
||||
),
|
||||
],
|
||||
child: BlocListener<ChatFileBloc, ChatFileState>(
|
||||
listenWhen: (previous, current) =>
|
||||
previous.indexFileIndicator != current.indexFileIndicator,
|
||||
listener: (context, state) {
|
||||
_handleIndexIndicator(state.indexFileIndicator, context);
|
||||
},
|
||||
child: BlocBuilder<ChatFileBloc, ChatFileState>(
|
||||
builder: (context, state) {
|
||||
return DropTarget(
|
||||
onDragDone: (DropDoneDetails detail) async {
|
||||
if (state.supportChatWithFile) {
|
||||
await showConfirmDialog(
|
||||
context: context,
|
||||
style: ConfirmPopupStyle.cancelAndOk,
|
||||
title: LocaleKeys.chat_chatWithFilePrompt.tr(),
|
||||
confirmLabel: LocaleKeys.button_confirm.tr(),
|
||||
onConfirm: () {
|
||||
for (final file in detail.files) {
|
||||
context
|
||||
.read<ChatFileBloc>()
|
||||
.add(ChatFileEvent.newFile(file.path, file.name));
|
||||
}
|
||||
},
|
||||
description: '',
|
||||
);
|
||||
}
|
||||
},
|
||||
child: _ChatContentPage(
|
||||
view: view,
|
||||
userProfile: userProfile,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
||||
fontSize: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleIndexIndicator(
|
||||
IndexFileIndicator? indicator,
|
||||
BuildContext context,
|
||||
) {
|
||||
if (indicator != null) {
|
||||
indicator.when(
|
||||
finish: (fileName) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.chat_indexFileSuccess.tr(args: [fileName]),
|
||||
);
|
||||
},
|
||||
indexing: (fileName) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
LocaleKeys.chat_indexingFile.tr(args: [fileName]),
|
||||
duration: const Duration(seconds: 2),
|
||||
);
|
||||
},
|
||||
error: (err) {
|
||||
showSnackBarMessage(
|
||||
context,
|
||||
err,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
State<AIChatPage> createState() => _AIChatPageState();
|
||||
}
|
||||
|
||||
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> {
|
||||
class _AIChatPageState extends State<AIChatPage> {
|
||||
late types.User _user;
|
||||
|
||||
@override
|
||||
@@ -188,27 +77,31 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) {
|
||||
return buildChatWidget();
|
||||
} else {
|
||||
return Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
||||
fontSize: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Center(
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_unsupportedCloudPrompt.tr(),
|
||||
fontSize: 20,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildChatWidget() {
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Flexible(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 784),
|
||||
child: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (blocContext, state) => Chat(
|
||||
return SizedBox.expand(
|
||||
child: Padding(
|
||||
padding: AIChatUILayout.chatPadding,
|
||||
child: BlocProvider(
|
||||
create: (context) => ChatBloc(
|
||||
view: widget.view,
|
||||
userProfile: widget.userProfile,
|
||||
)..add(const ChatEvent.initialLoad()),
|
||||
child: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (blocContext, state) {
|
||||
return Chat(
|
||||
messages: state.messages,
|
||||
onSendPressed: (_) {
|
||||
onAttachmentPressed: () {},
|
||||
onSendPressed: (types.PartialText message) {
|
||||
// We use custom bottom widget for chat input, so
|
||||
// do not need to handle this event.
|
||||
},
|
||||
@@ -225,27 +118,32 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
}
|
||||
},
|
||||
emptyState: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (_, state) =>
|
||||
state.initialLoadingStatus == const LoadingState.finish()
|
||||
? Padding(
|
||||
padding: AIChatUILayout.welcomePagePadding,
|
||||
child: ChatWelcomePage(
|
||||
onSelectedQuestion: (question) => blocContext
|
||||
builder: (context, state) {
|
||||
return 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(),
|
||||
.add(ChatEvent.sendMessage(question));
|
||||
},
|
||||
),
|
||||
)
|
||||
: const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
},
|
||||
),
|
||||
messageWidthRatio: AIChatUILayout.messageWidthRatio,
|
||||
textMessageBuilder: (
|
||||
textMessage, {
|
||||
required messageWidth,
|
||||
required showName,
|
||||
}) =>
|
||||
_buildAITextMessage(blocContext, textMessage),
|
||||
}) {
|
||||
return _buildAITextMessage(blocContext, textMessage);
|
||||
},
|
||||
bubbleBuilder: (
|
||||
child, {
|
||||
required message,
|
||||
@@ -256,15 +154,15 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
message: message,
|
||||
child: child,
|
||||
);
|
||||
} else {
|
||||
return _buildAIBubble(message, blocContext, state, child);
|
||||
}
|
||||
|
||||
return _buildAIBubble(message, blocContext, state, child);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -391,43 +289,31 @@ class _ChatContentPageState extends State<_ChatContentPage> {
|
||||
return ClipRect(
|
||||
child: Padding(
|
||||
padding: AIChatUILayout.safeAreaInsets(context),
|
||||
child: BlocBuilder<ChatInputBloc, ChatInputState>(
|
||||
builder: (context, state) {
|
||||
final hintText = state.aiType.when(
|
||||
appflowyAI: () => LocaleKeys.chat_inputMessageHint.tr(),
|
||||
localAI: () => LocaleKeys.chat_inputLocalAIMessageHint.tr(),
|
||||
);
|
||||
|
||||
return 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());
|
||||
},
|
||||
hintText: hintText,
|
||||
);
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const VSpace(6),
|
||||
Opacity(
|
||||
opacity: 0.6,
|
||||
child: FlowyText(
|
||||
LocaleKeys.chat_aiMistakePrompt.tr(),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
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/chat_input.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';
|
||||
@@ -182,20 +176,9 @@ class CopyButton extends StatelessWidget {
|
||||
size: const Size.square(14),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
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(),
|
||||
);
|
||||
}
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: textMessage.text));
|
||||
showMessageToast(LocaleKeys.grid_row_copyProperty.tr());
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
-237
@@ -1,237 +0,0 @@
|
||||
import 'package:flowy_infra_ui/style_widget/button.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
abstract class ChatActionMenuItem {
|
||||
String get title;
|
||||
}
|
||||
|
||||
abstract class ChatActionHandler {
|
||||
List<ChatActionMenuItem> get items;
|
||||
void onEnter();
|
||||
void onSelected(ChatActionMenuItem item);
|
||||
void onExit();
|
||||
}
|
||||
|
||||
abstract class ChatAnchor {
|
||||
GlobalKey get anchorKey;
|
||||
LayerLink get layerLink;
|
||||
}
|
||||
|
||||
const int _itemHeight = 34;
|
||||
const int _itemVerticalPadding = 4;
|
||||
|
||||
class ChatActionsMenu {
|
||||
ChatActionsMenu({
|
||||
required this.anchor,
|
||||
required this.context,
|
||||
required this.handler,
|
||||
required this.style,
|
||||
});
|
||||
|
||||
final BuildContext context;
|
||||
final ChatAnchor anchor;
|
||||
final ChatActionsMenuStyle style;
|
||||
final ChatActionHandler handler;
|
||||
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
void dismiss() {
|
||||
_overlayEntry?.remove();
|
||||
_overlayEntry = null;
|
||||
handler.onExit();
|
||||
}
|
||||
|
||||
void show() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _show());
|
||||
}
|
||||
|
||||
void _show() {
|
||||
if (_overlayEntry != null) {
|
||||
dismiss();
|
||||
}
|
||||
|
||||
if (anchor.anchorKey.currentContext == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
handler.onEnter();
|
||||
|
||||
final height = handler.items.length * (_itemHeight + _itemVerticalPadding);
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => Stack(
|
||||
children: [
|
||||
CompositedTransformFollower(
|
||||
link: anchor.layerLink,
|
||||
showWhenUnlinked: false,
|
||||
offset: Offset(0, -height - 4),
|
||||
child: Material(
|
||||
elevation: 4.0,
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minWidth: 200,
|
||||
maxWidth: 200,
|
||||
maxHeight: 200,
|
||||
),
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
),
|
||||
child: ActionList(
|
||||
handler: handler,
|
||||
onDismiss: () => dismiss(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
Overlay.of(context).insert(_overlayEntry!);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActionItem extends StatelessWidget {
|
||||
const _ActionItem({
|
||||
required this.item,
|
||||
required this.onTap,
|
||||
required this.isSelected,
|
||||
});
|
||||
|
||||
final ChatActionMenuItem item;
|
||||
final VoidCallback? onTap;
|
||||
final bool isSelected;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: _itemHeight.toDouble(),
|
||||
padding: const EdgeInsets.symmetric(vertical: _itemVerticalPadding / 2.0),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary.withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(4.0),
|
||||
),
|
||||
child: FlowyButton(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 6),
|
||||
iconPadding: 10.0,
|
||||
text: FlowyText.regular(
|
||||
item.title,
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ActionList extends StatefulWidget {
|
||||
const ActionList({super.key, required this.handler, required this.onDismiss});
|
||||
|
||||
final ChatActionHandler handler;
|
||||
final VoidCallback? onDismiss;
|
||||
|
||||
@override
|
||||
State<ActionList> createState() => _ActionListState();
|
||||
}
|
||||
|
||||
class _ActionListState extends State<ActionList> {
|
||||
final FocusScopeNode _focusNode =
|
||||
FocusScopeNode(debugLabel: 'ChatActionsMenu');
|
||||
int _selectedIndex = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_focusNode.requestFocus();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_focusNode.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _handleKeyPress(event) {
|
||||
setState(() {
|
||||
// ignore: deprecated_member_use
|
||||
if (event is KeyDownEvent || event is RawKeyDownEvent) {
|
||||
if (event.logicalKey == LogicalKeyboardKey.arrowDown) {
|
||||
_selectedIndex = (_selectedIndex + 1) % widget.handler.items.length;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.arrowUp) {
|
||||
_selectedIndex = (_selectedIndex - 1 + widget.handler.items.length) %
|
||||
widget.handler.items.length;
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.enter) {
|
||||
widget.handler.onSelected(widget.handler.items[_selectedIndex]);
|
||||
widget.onDismiss?.call();
|
||||
} else if (event.logicalKey == LogicalKeyboardKey.escape) {
|
||||
widget.onDismiss?.call();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FocusScope(
|
||||
node: _focusNode,
|
||||
onKey: (node, event) {
|
||||
_handleKeyPress(event);
|
||||
return KeyEventResult.handled;
|
||||
},
|
||||
child: ListView(
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.all(8),
|
||||
children: widget.handler.items.asMap().entries.map((entry) {
|
||||
final index = entry.key;
|
||||
final ChatActionMenuItem item = entry.value;
|
||||
return _ActionItem(
|
||||
item: item,
|
||||
onTap: () {
|
||||
widget.handler.onSelected(item);
|
||||
widget.onDismiss?.call();
|
||||
},
|
||||
isSelected: _selectedIndex == index,
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChatActionsMenuStyle {
|
||||
ChatActionsMenuStyle({
|
||||
required this.backgroundColor,
|
||||
required this.groupTextColor,
|
||||
required this.menuItemTextColor,
|
||||
required this.menuItemSelectedColor,
|
||||
required this.menuItemSelectedTextColor,
|
||||
});
|
||||
|
||||
const ChatActionsMenuStyle.light()
|
||||
: backgroundColor = Colors.white,
|
||||
groupTextColor = const Color(0xFF555555),
|
||||
menuItemTextColor = const Color(0xFF333333),
|
||||
menuItemSelectedColor = const Color(0xFFE0F8FF),
|
||||
menuItemSelectedTextColor = const Color.fromARGB(255, 56, 91, 247);
|
||||
|
||||
const ChatActionsMenuStyle.dark()
|
||||
: backgroundColor = const Color(0xFF282E3A),
|
||||
groupTextColor = const Color(0xFFBBC3CD),
|
||||
menuItemTextColor = const Color(0xFFBBC3CD),
|
||||
menuItemSelectedColor = const Color(0xFF00BCF0),
|
||||
menuItemSelectedTextColor = const Color(0xFF131720);
|
||||
|
||||
final Color backgroundColor;
|
||||
final Color groupTextColor;
|
||||
final Color menuItemTextColor;
|
||||
final Color menuItemSelectedColor;
|
||||
final Color menuItemSelectedTextColor;
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
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/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
|
||||
class ChatInput extends StatefulWidget {
|
||||
/// Creates [ChatInput] widget.
|
||||
const ChatInput({
|
||||
super.key,
|
||||
this.isAttachmentUploading,
|
||||
this.onAttachmentPressed,
|
||||
required this.onSendPressed,
|
||||
required this.chatId,
|
||||
this.options = const InputOptions(),
|
||||
required this.isStreaming,
|
||||
required this.onStopStreaming,
|
||||
});
|
||||
|
||||
final bool? isAttachmentUploading;
|
||||
final VoidCallback? onAttachmentPressed;
|
||||
final void Function(types.PartialText) onSendPressed;
|
||||
final void Function() onStopStreaming;
|
||||
final InputOptions options;
|
||||
final String chatId;
|
||||
final bool isStreaming;
|
||||
|
||||
@override
|
||||
State<ChatInput> createState() => _ChatInputState();
|
||||
}
|
||||
|
||||
/// [ChatInput] widget state.
|
||||
class _ChatInputState extends State<ChatInput> {
|
||||
late final _inputFocusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (event.physicalKey == PhysicalKeyboardKey.enter &&
|
||||
!HardwareKeyboard.instance.physicalKeysPressed.any(
|
||||
(el) => <PhysicalKeyboardKey>{
|
||||
PhysicalKeyboardKey.shiftLeft,
|
||||
PhysicalKeyboardKey.shiftRight,
|
||||
}.contains(el),
|
||||
)) {
|
||||
if (kIsWeb && _textController.value.isComposingRangeValid) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (event is KeyDownEvent) {
|
||||
_handleSendPressed();
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
bool _sendButtonVisible = false;
|
||||
late TextEditingController _textController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_textController =
|
||||
widget.options.textEditingController ?? InputTextFieldController();
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
|
||||
void _handleSendButtonVisibilityModeChange() {
|
||||
_textController.removeListener(_handleTextControllerChange);
|
||||
_sendButtonVisible =
|
||||
_textController.text.trim() != '' || widget.isStreaming;
|
||||
_textController.addListener(_handleTextControllerChange);
|
||||
}
|
||||
|
||||
void _handleSendPressed() {
|
||||
if (widget.isStreaming) {
|
||||
widget.onStopStreaming();
|
||||
} else {
|
||||
final trimmedText = _textController.text.trim();
|
||||
if (trimmedText != '') {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
|
||||
if (widget.options.inputClearMode == InputClearMode.always) {
|
||||
_textController.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTextControllerChange() {
|
||||
if (_textController.value.isComposingRangeValid) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_sendButtonVisible = _textController.text.trim() != '';
|
||||
});
|
||||
}
|
||||
|
||||
Widget _inputBuilder() {
|
||||
const textPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
|
||||
const buttonPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
|
||||
const inputPadding = EdgeInsets.all(6);
|
||||
|
||||
return Focus(
|
||||
autofocus: !widget.options.autofocus,
|
||||
child: Padding(
|
||||
padding: inputPadding,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: isMobile
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
elevation: 0.6,
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.onAttachmentPressed != null)
|
||||
AttachmentButton(
|
||||
isLoading: widget.isAttachmentUploading ?? false,
|
||||
onPressed: widget.onAttachmentPressed,
|
||||
padding: buttonPadding,
|
||||
),
|
||||
Expanded(child: _inputTextField(textPadding)),
|
||||
_sendButton(buttonPadding),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Padding _inputTextField(EdgeInsets textPadding) {
|
||||
return Padding(
|
||||
padding: textPadding,
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
readOnly: widget.isStreaming,
|
||||
focusNode: _inputFocusNode,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: LocaleKeys.chat_inputMessageHint.tr(),
|
||||
hintStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
enabled: widget.options.enabled,
|
||||
autocorrect: widget.options.autocorrect,
|
||||
autofocus: widget.options.autofocus,
|
||||
enableSuggestions: widget.options.enableSuggestions,
|
||||
keyboardType: widget.options.keyboardType,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
maxLines: 10,
|
||||
minLines: 1,
|
||||
onChanged: widget.options.onTextChanged,
|
||||
onTap: widget.options.onTextFieldTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ConstrainedBox _sendButton(EdgeInsets buttonPadding) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: buttonPadding.bottom + buttonPadding.top + 24,
|
||||
),
|
||||
child: Visibility(
|
||||
visible: _sendButtonVisible,
|
||||
child: Padding(
|
||||
padding: buttonPadding,
|
||||
child: AccessoryButton(
|
||||
onSendPressed: () {
|
||||
_handleSendPressed();
|
||||
},
|
||||
onStopStreaming: () {
|
||||
widget.onStopStreaming();
|
||||
},
|
||||
isStreaming: widget.isStreaming,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ChatInput oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_inputFocusNode.dispose();
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: () => _inputFocusNode.requestFocus(),
|
||||
child: _inputBuilder(),
|
||||
);
|
||||
}
|
||||
|
||||
@immutable
|
||||
class InputOptions {
|
||||
const InputOptions({
|
||||
this.inputClearMode = InputClearMode.always,
|
||||
this.keyboardType = TextInputType.multiline,
|
||||
this.onTextChanged,
|
||||
this.onTextFieldTap,
|
||||
this.textEditingController,
|
||||
this.autocorrect = true,
|
||||
this.autofocus = false,
|
||||
this.enableSuggestions = true,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
/// Controls the [ChatInput] clear behavior. Defaults to [InputClearMode.always].
|
||||
final InputClearMode inputClearMode;
|
||||
|
||||
/// Controls the [ChatInput] keyboard type. Defaults to [TextInputType.multiline].
|
||||
final TextInputType keyboardType;
|
||||
|
||||
/// Will be called whenever the text inside [TextField] changes.
|
||||
final void Function(String)? onTextChanged;
|
||||
|
||||
/// Will be called on [TextField] tap.
|
||||
final VoidCallback? onTextFieldTap;
|
||||
|
||||
/// Custom [TextEditingController]. If not provided, defaults to the
|
||||
/// [InputTextFieldController], which extends [TextEditingController] and has
|
||||
/// additional fatures like markdown support. If you want to keep additional
|
||||
/// features but still need some methods from the default [TextEditingController],
|
||||
/// you can create your own [InputTextFieldController] (imported from this lib)
|
||||
/// and pass it here.
|
||||
final TextEditingController? textEditingController;
|
||||
|
||||
/// Controls the [TextInput] autocorrect behavior. Defaults to [true].
|
||||
final bool autocorrect;
|
||||
|
||||
/// Whether [TextInput] should have focus. Defaults to [false].
|
||||
final bool autofocus;
|
||||
|
||||
/// Controls the [TextInput] enableSuggestions behavior. Defaults to [true].
|
||||
final bool enableSuggestions;
|
||||
|
||||
/// Controls the [TextInput] enabled behavior. Defaults to [true].
|
||||
final bool enabled;
|
||||
}
|
||||
|
||||
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
class AccessoryButton extends StatelessWidget {
|
||||
const AccessoryButton({
|
||||
required this.onSendPressed,
|
||||
required this.onStopStreaming,
|
||||
required this.isStreaming,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final void Function() onSendPressed;
|
||||
final void Function() onStopStreaming;
|
||||
final bool isStreaming;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isStreaming) {
|
||||
return FlowyIconButton(
|
||||
width: 36,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_stream_stop_s,
|
||||
size: const Size.square(28),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onStopStreaming,
|
||||
radius: BorderRadius.circular(18),
|
||||
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
);
|
||||
} else {
|
||||
return FlowyIconButton(
|
||||
width: 36,
|
||||
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
radius: BorderRadius.circular(18),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.send_s,
|
||||
size: const Size.square(24),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onSendPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-48
@@ -1,48 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatInputAccessoryButton extends StatelessWidget {
|
||||
const ChatInputAccessoryButton({
|
||||
required this.onSendPressed,
|
||||
required this.onStopStreaming,
|
||||
required this.isStreaming,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final void Function() onSendPressed;
|
||||
final void Function() onStopStreaming;
|
||||
final bool isStreaming;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (isStreaming) {
|
||||
return FlowyIconButton(
|
||||
width: 36,
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.ai_stream_stop_s,
|
||||
size: const Size.square(28),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onStopStreaming,
|
||||
radius: BorderRadius.circular(18),
|
||||
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
);
|
||||
} else {
|
||||
return FlowyIconButton(
|
||||
width: 36,
|
||||
fillColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
hoverColor: AFThemeExtension.of(context).lightGreyHover,
|
||||
radius: BorderRadius.circular(18),
|
||||
icon: FlowySvg(
|
||||
FlowySvgs.send_s,
|
||||
size: const Size.square(24),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
onPressed: onSendPressed,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
-76
@@ -1,76 +0,0 @@
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_inline_action_menu.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class ChatTextFieldInterceptor {
|
||||
String previosText = "";
|
||||
|
||||
ChatActionHandler? onTextChanged(
|
||||
String text,
|
||||
TextEditingController textController,
|
||||
FocusNode textFieldFocusNode,
|
||||
) {
|
||||
if (previosText == "/" && text == "/ ") {
|
||||
final handler = IndexActionHandler(
|
||||
textController: textController,
|
||||
textFieldFocusNode: textFieldFocusNode,
|
||||
) as ChatActionHandler;
|
||||
return handler;
|
||||
}
|
||||
previosText = text;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
class FixGrammarMenuItem extends ChatActionMenuItem {
|
||||
@override
|
||||
String get title => "Fix Grammar";
|
||||
}
|
||||
|
||||
class ImproveWritingMenuItem extends ChatActionMenuItem {
|
||||
@override
|
||||
String get title => "Improve Writing";
|
||||
}
|
||||
|
||||
class ChatWithFileMenuItem extends ChatActionMenuItem {
|
||||
@override
|
||||
String get title => "Chat With PDF";
|
||||
}
|
||||
|
||||
class IndexActionHandler extends ChatActionHandler {
|
||||
IndexActionHandler({
|
||||
required this.textController,
|
||||
required this.textFieldFocusNode,
|
||||
});
|
||||
|
||||
final TextEditingController textController;
|
||||
final FocusNode textFieldFocusNode;
|
||||
|
||||
@override
|
||||
List<ChatActionMenuItem> get items => [
|
||||
ChatWithFileMenuItem(),
|
||||
FixGrammarMenuItem(),
|
||||
ImproveWritingMenuItem(),
|
||||
];
|
||||
|
||||
@override
|
||||
void onSelected(ChatActionMenuItem item) {
|
||||
textController.clear();
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => textFieldFocusNode.requestFocus(),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onExit() {
|
||||
if (!textFieldFocusNode.hasFocus) {
|
||||
textFieldFocusNode.requestFocus();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void onEnter() {
|
||||
if (textFieldFocusNode.hasFocus) {
|
||||
textFieldFocusNode.unfocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
-240
@@ -1,240 +0,0 @@
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_inline_action_menu.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
import 'package:flutter_chat_ui/flutter_chat_ui.dart';
|
||||
|
||||
import 'chat_accessory_button.dart';
|
||||
|
||||
class ChatInput extends StatefulWidget {
|
||||
/// Creates [ChatInput] widget.
|
||||
const ChatInput({
|
||||
super.key,
|
||||
this.isAttachmentUploading,
|
||||
this.onAttachmentPressed,
|
||||
required this.onSendPressed,
|
||||
required this.chatId,
|
||||
this.options = const InputOptions(),
|
||||
required this.isStreaming,
|
||||
required this.onStopStreaming,
|
||||
required this.hintText,
|
||||
});
|
||||
|
||||
final bool? isAttachmentUploading;
|
||||
final VoidCallback? onAttachmentPressed;
|
||||
final void Function(types.PartialText) onSendPressed;
|
||||
final void Function() onStopStreaming;
|
||||
final InputOptions options;
|
||||
final String chatId;
|
||||
final bool isStreaming;
|
||||
final String hintText;
|
||||
|
||||
@override
|
||||
State<ChatInput> createState() => _ChatInputState();
|
||||
}
|
||||
|
||||
/// [ChatInput] widget state.
|
||||
class _ChatInputState extends State<ChatInput> {
|
||||
final GlobalKey _textFieldKey = GlobalKey();
|
||||
final LayerLink _layerLink = LayerLink();
|
||||
// final ChatTextFieldInterceptor _textFieldInterceptor =
|
||||
// ChatTextFieldInterceptor();
|
||||
|
||||
late final _inputFocusNode = FocusNode(
|
||||
onKeyEvent: (node, event) {
|
||||
if (event.physicalKey == PhysicalKeyboardKey.enter &&
|
||||
!HardwareKeyboard.instance.physicalKeysPressed.any(
|
||||
(el) => <PhysicalKeyboardKey>{
|
||||
PhysicalKeyboardKey.shiftLeft,
|
||||
PhysicalKeyboardKey.shiftRight,
|
||||
}.contains(el),
|
||||
)) {
|
||||
if (kIsWeb && _textController.value.isComposingRangeValid) {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
if (event is KeyDownEvent) {
|
||||
if (!widget.isStreaming) {
|
||||
_handleSendPressed();
|
||||
}
|
||||
}
|
||||
return KeyEventResult.handled;
|
||||
} else {
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
},
|
||||
);
|
||||
late TextEditingController _textController;
|
||||
|
||||
bool _sendButtonVisible = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_textController = InputTextFieldController();
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_inputFocusNode.dispose();
|
||||
_textController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
const textPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
|
||||
const buttonPadding = EdgeInsets.symmetric(horizontal: 16, vertical: 6);
|
||||
const inputPadding = EdgeInsets.all(6);
|
||||
|
||||
return Focus(
|
||||
child: Padding(
|
||||
padding: inputPadding,
|
||||
child: Material(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: isMobile
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
elevation: 0.6,
|
||||
child: Row(
|
||||
children: [
|
||||
if (widget.onAttachmentPressed != null)
|
||||
AttachmentButton(
|
||||
isLoading: widget.isAttachmentUploading ?? false,
|
||||
onPressed: widget.onAttachmentPressed,
|
||||
padding: buttonPadding,
|
||||
),
|
||||
Expanded(child: _inputTextField(textPadding)),
|
||||
_sendButton(buttonPadding),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _handleSendButtonVisibilityModeChange() {
|
||||
_textController.removeListener(_handleTextControllerChange);
|
||||
_sendButtonVisible =
|
||||
_textController.text.trim() != '' || widget.isStreaming;
|
||||
_textController.addListener(_handleTextControllerChange);
|
||||
}
|
||||
|
||||
void _handleSendPressed() {
|
||||
final trimmedText = _textController.text.trim();
|
||||
if (trimmedText != '') {
|
||||
final partialText = types.PartialText(text: trimmedText);
|
||||
widget.onSendPressed(partialText);
|
||||
|
||||
_textController.clear();
|
||||
}
|
||||
}
|
||||
|
||||
void _handleTextControllerChange() {
|
||||
if (_textController.value.isComposingRangeValid) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_sendButtonVisible = _textController.text.trim() != '';
|
||||
});
|
||||
}
|
||||
|
||||
Widget _inputTextField(EdgeInsets textPadding) {
|
||||
return CompositedTransformTarget(
|
||||
link: _layerLink,
|
||||
child: Padding(
|
||||
padding: textPadding,
|
||||
child: TextField(
|
||||
key: _textFieldKey,
|
||||
controller: _textController,
|
||||
focusNode: _inputFocusNode,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
hintText: widget.hintText,
|
||||
hintStyle: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor.withOpacity(0.5),
|
||||
),
|
||||
),
|
||||
style: TextStyle(
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
),
|
||||
keyboardType: TextInputType.multiline,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
maxLines: 10,
|
||||
minLines: 1,
|
||||
// onChanged: (text) {
|
||||
// final handler = _textFieldInterceptor.onTextChanged(
|
||||
// text,
|
||||
// _textController,
|
||||
// _inputFocusNode,
|
||||
// );
|
||||
// // If the handler is not null, it means that the text has been
|
||||
// // recognized as a command.
|
||||
// if (handler != null) {
|
||||
// ChatActionsMenu(
|
||||
// anchor: ChatInputAnchor(
|
||||
// anchorKey: _textFieldKey,
|
||||
// layerLink: _layerLink,
|
||||
// ),
|
||||
// handler: handler,
|
||||
// context: context,
|
||||
// style: Theme.of(context).brightness == Brightness.dark
|
||||
// ? const ChatActionsMenuStyle.dark()
|
||||
// : const ChatActionsMenuStyle.light(),
|
||||
// ).show();
|
||||
// }
|
||||
// },
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ConstrainedBox _sendButton(EdgeInsets buttonPadding) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
minHeight: buttonPadding.bottom + buttonPadding.top + 24,
|
||||
),
|
||||
child: Visibility(
|
||||
visible: _sendButtonVisible,
|
||||
child: Padding(
|
||||
padding: buttonPadding,
|
||||
child: ChatInputAccessoryButton(
|
||||
onSendPressed: () {
|
||||
if (!widget.isStreaming) {
|
||||
widget.onStopStreaming();
|
||||
_handleSendPressed();
|
||||
}
|
||||
},
|
||||
onStopStreaming: () => widget.onStopStreaming(),
|
||||
isStreaming: widget.isStreaming,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant ChatInput oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_handleSendButtonVisibilityModeChange();
|
||||
}
|
||||
}
|
||||
|
||||
final isMobile = defaultTargetPlatform == TargetPlatform.android ||
|
||||
defaultTargetPlatform == TargetPlatform.iOS;
|
||||
|
||||
class ChatInputAnchor extends ChatAnchor {
|
||||
ChatInputAnchor({
|
||||
required this.anchorKey,
|
||||
required this.layerLink,
|
||||
});
|
||||
|
||||
@override
|
||||
final GlobalKey<State<StatefulWidget>> anchorKey;
|
||||
|
||||
@override
|
||||
final LayerLink layerLink;
|
||||
}
|
||||
+1
-1
@@ -6,6 +6,7 @@ import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
|
||||
class RelatedQuestionList extends StatelessWidget {
|
||||
const RelatedQuestionList({
|
||||
required this.chatId,
|
||||
@@ -96,7 +97,6 @@ class _RelatedQuestionItemState extends State<RelatedQuestionItem> {
|
||||
style: TextStyle(
|
||||
color: _isHovered ? Theme.of(context).colorScheme.primary : null,
|
||||
fontSize: 14,
|
||||
height: 1.5,
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
|
||||
@@ -5,7 +5,7 @@ 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/chat_input.dart';
|
||||
import 'chat_input.dart';
|
||||
|
||||
class ChatWelcomePage extends StatelessWidget {
|
||||
ChatWelcomePage({required this.onSelectedQuestion, super.key});
|
||||
@@ -31,15 +31,19 @@ class ChatWelcomePage extends StatelessWidget {
|
||||
size: Size.square(44),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
Wrap(
|
||||
children: items
|
||||
.map(
|
||||
(i) => WelcomeQuestion(
|
||||
question: i,
|
||||
onSelected: onSelectedQuestion,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: isMobile ? 2 : 4,
|
||||
crossAxisSpacing: 6,
|
||||
mainAxisSpacing: 6,
|
||||
childAspectRatio: 16.0 / 9.0,
|
||||
),
|
||||
itemCount: items.length,
|
||||
itemBuilder: (context, index) => WelcomeQuestion(
|
||||
question: items[index],
|
||||
onSelected: onSelectedQuestion,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
-382
@@ -1,382 +0,0 @@
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
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_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.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 BlocProvider(
|
||||
create: (context) => DocumentPageStyleBloc(view: ViewPB())
|
||||
..add(const DocumentPageStyleEvent.initial()),
|
||||
child: _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 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) {
|
||||
// don't lazy load the styleCustomizer and blockBuilders,
|
||||
// it needs the context to get the theme.
|
||||
final styleCustomizer = EditorStyleCustomizer(
|
||||
context: context,
|
||||
padding: EdgeInsets.zero,
|
||||
);
|
||||
final editorStyle = styleCustomizer.style().copyWith(
|
||||
// hide the cursor
|
||||
cursorColor: Colors.transparent,
|
||||
cursorWidth: 0,
|
||||
);
|
||||
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,
|
||||
);
|
||||
}
|
||||
+261
-29
@@ -1,15 +1,20 @@
|
||||
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/plugins/ai_chat/presentation/message/ai_markdown_text.dart';
|
||||
import 'package:appflowy/util/theme_extension.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({
|
||||
@@ -37,38 +42,265 @@ class ChatAITextMessageWidget extends StatelessWidget {
|
||||
)..add(const ChatAIMessageEvent.initial()),
|
||||
child: BlocBuilder<ChatAIMessageBloc, ChatAIMessageState>(
|
||||
builder: (context, state) {
|
||||
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();
|
||||
},
|
||||
);
|
||||
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);
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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,6 +52,7 @@ class TextMessageText extends StatelessWidget {
|
||||
text,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
lineHeight: 1.5,
|
||||
maxLines: null,
|
||||
selectable: true,
|
||||
color: AFThemeExtension.of(context).textColor,
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_header.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_search_bar.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
|
||||
import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
|
||||
// use a global value to store the selected emoji to prevent reloading every time.
|
||||
@@ -38,7 +37,9 @@ class _FlowyEmojiPickerState extends State<FlowyEmojiPicker> {
|
||||
EmojiData.builtIn().then(
|
||||
(value) {
|
||||
kCachedEmojiData = value;
|
||||
setState(() => emojiData = value);
|
||||
setState(() {
|
||||
emojiData = value;
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,6 @@ class BlankPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) =>
|
||||
const BlankPage();
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/view/view_cache.dart';
|
||||
import 'package:appflowy/plugins/database/domain/database_view_service.dart';
|
||||
@@ -14,6 +12,7 @@ import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'defines.dart';
|
||||
import 'row/row_cache.dart';
|
||||
@@ -224,7 +223,6 @@ class DatabaseController {
|
||||
_databaseCallbacks.clear();
|
||||
_groupCallbacks.clear();
|
||||
_layoutCallbacks.clear();
|
||||
_isLoading.dispose();
|
||||
}
|
||||
|
||||
Future<void> _loadGroups() async {
|
||||
|
||||
+6
-11
@@ -1,5 +1,7 @@
|
||||
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';
|
||||
@@ -17,9 +19,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 {
|
||||
@@ -74,6 +76,7 @@ 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),
|
||||
@@ -443,13 +446,9 @@ 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;
|
||||
var updatedField = newFields.firstOrNull;
|
||||
|
||||
if (updatedField == null) {
|
||||
return null;
|
||||
}
|
||||
FieldInfo updatedField = newFields[0];
|
||||
|
||||
final index = newFields
|
||||
.indexWhere((field) => field.id == updatedFieldSettings.fieldId);
|
||||
@@ -471,10 +470,6 @@ 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.deleteView(viewId: viewId);
|
||||
final result = await ViewBackendService.delete(viewId: viewId);
|
||||
result.fold(
|
||||
(l) {},
|
||||
(r) => Log.error(r),
|
||||
|
||||
+10
-5
@@ -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/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
@@ -21,12 +19,12 @@ import 'package:flowy_infra/size.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
|
||||
import '../../application/row/row_controller.dart';
|
||||
import '../../widgets/row/row_detail.dart';
|
||||
|
||||
import 'calendar_day.dart';
|
||||
import 'layout/sizes.dart';
|
||||
import 'toolbar/calendar_setting_bar.dart';
|
||||
@@ -92,11 +90,12 @@ class _CalendarPageState extends State<CalendarPage> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_calendarState = GlobalKey<MonthViewState>();
|
||||
_calendarBloc = CalendarBloc(
|
||||
databaseController: widget.databaseController,
|
||||
)..add(const CalendarEvent.initial());
|
||||
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -379,7 +378,13 @@ class UnscheduledEventsButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _UnscheduledEventsButtonState extends State<UnscheduledEventsButton> {
|
||||
final PopoverController _popoverController = PopoverController();
|
||||
late final PopoverController _popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_popoverController = PopoverController();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
+3
-8
@@ -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/database_controller.dart';
|
||||
@@ -7,10 +5,12 @@ import 'package:appflowy/plugins/database/application/setting/property_bloc.dart
|
||||
import 'package:appflowy/plugins/database/calendar/application/calendar_setting_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle_style.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
/// Widget that displays a list of settings that alters the appearance of the
|
||||
@@ -30,12 +30,6 @@ class CalendarLayoutSetting extends StatefulWidget {
|
||||
class _CalendarLayoutSettingState extends State<CalendarLayoutSetting> {
|
||||
final PopoverMutex popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider(
|
||||
@@ -331,6 +325,7 @@ Widget _toggleItem({
|
||||
Toggle(
|
||||
value: value,
|
||||
onChanged: (value) => onToggle(!value),
|
||||
style: ToggleStyle.big,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
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';
|
||||
@@ -6,7 +9,6 @@ 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';
|
||||
@@ -14,7 +16,6 @@ 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';
|
||||
|
||||
@@ -23,6 +24,7 @@ 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';
|
||||
@@ -502,10 +504,7 @@ class _PositionedCalculationsRowState
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(
|
||||
left:
|
||||
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
|
||||
),
|
||||
margin: EdgeInsets.only(left: GridSize.horizontalHeaderPadding + 40),
|
||||
padding: const EdgeInsets.only(bottom: 10),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).canvasColor,
|
||||
|
||||
+3
-5
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/application/calculations/calculation_type_ext.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_info.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/type_option/number_format_bloc.dart';
|
||||
@@ -14,7 +16,6 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
class CalculateCell extends StatefulWidget {
|
||||
@@ -140,14 +141,11 @@ class _CalculateCellState extends State<CalculateCell> {
|
||||
TextSpan(
|
||||
text: widget.calculation!.calculationType.shortLabel
|
||||
.toUpperCase(),
|
||||
style: context.tooltipTextStyle(),
|
||||
),
|
||||
const TextSpan(text: ' '),
|
||||
TextSpan(
|
||||
text: calculateValue,
|
||||
style: context
|
||||
.tooltipTextStyle()
|
||||
?.copyWith(fontWeight: FontWeight.w500),
|
||||
style: const TextStyle(fontWeight: FontWeight.w500),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
+4
-11
@@ -1,19 +1,18 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/checkbox_filter_editor_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checkbox_filter.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class CheckboxFilterChoicechip extends StatefulWidget {
|
||||
@@ -31,9 +30,9 @@ class _CheckboxFilterChoicechipState extends State<CheckboxFilterChoicechip> {
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
bloc = CheckboxFilterEditorBloc(filterInfo: widget.filterInfo)
|
||||
..add(const CheckboxFilterEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -83,12 +82,6 @@ class CheckboxFilterEditor extends StatefulWidget {
|
||||
class _CheckboxFilterEditorState extends State<CheckboxFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
|
||||
+6
-8
@@ -1,14 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/checklist_filter_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/checklist_filter.pbenum.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../condition_button.dart';
|
||||
import '../../disclosure_button.dart';
|
||||
import '../../filter_info.dart';
|
||||
@@ -25,20 +23,20 @@ class ChecklistFilterChoicechip extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _ChecklistFilterChoicechipState extends State<ChecklistFilterChoicechip> {
|
||||
late final ChecklistFilterEditorBloc bloc;
|
||||
final PopoverMutex popoverMutex = PopoverMutex();
|
||||
late ChecklistFilterEditorBloc bloc;
|
||||
late PopoverMutex popoverMutex;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
popoverMutex = PopoverMutex();
|
||||
bloc = ChecklistFilterEditorBloc(filterInfo: widget.filterInfo);
|
||||
bloc.add(const ChecklistFilterEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
bloc.close();
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
|
||||
+1
-9
@@ -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/grid/application/filter/number_filter_editor_bloc.dart';
|
||||
@@ -8,12 +6,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class NumberFilterChoiceChip extends StatefulWidget {
|
||||
@@ -66,12 +64,6 @@ class NumberFilterEditor extends StatefulWidget {
|
||||
class _NumberFilterEditorState extends State<NumberFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<NumberFilterEditorBloc, NumberFilterEditorState>(
|
||||
|
||||
+2
-10
@@ -1,16 +1,14 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/plugins/database/grid/application/filter/select_option_filter_bloc.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/select_option_filter.pb.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../disclosure_button.dart';
|
||||
import '../../filter_info.dart';
|
||||
import '../choicechip.dart';
|
||||
|
||||
import 'condition_list.dart';
|
||||
import 'option_list.dart';
|
||||
import 'select_option_loader.dart';
|
||||
@@ -31,7 +29,6 @@ class _SelectOptionFilterChoicechipState
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
if (widget.filterInfo.fieldInfo.fieldType == FieldType.SingleSelect) {
|
||||
bloc = SelectOptionFilterEditorBloc(
|
||||
filterInfo: widget.filterInfo,
|
||||
@@ -46,6 +43,7 @@ class _SelectOptionFilterChoicechipState
|
||||
);
|
||||
}
|
||||
bloc.add(const SelectOptionFilterEditorEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -92,12 +90,6 @@ class SelectOptionFilterEditor extends StatefulWidget {
|
||||
class _SelectOptionFilterEditorState extends State<SelectOptionFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
|
||||
+1
-9
@@ -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/grid/application/filter/text_filter_editor_bloc.dart';
|
||||
@@ -8,12 +6,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class TextFilterChoicechip extends StatelessWidget {
|
||||
@@ -74,12 +72,6 @@ class TextFilterEditor extends StatefulWidget {
|
||||
class _TextFilterEditorState extends State<TextFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TextFilterEditorBloc, TextFilterEditorState>(
|
||||
|
||||
+1
-9
@@ -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/grid/application/filter/time_filter_editor_bloc.dart';
|
||||
@@ -8,12 +6,12 @@ import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../condition_button.dart';
|
||||
import '../disclosure_button.dart';
|
||||
import '../filter_info.dart';
|
||||
|
||||
import 'choicechip.dart';
|
||||
|
||||
class TimeFilterChoiceChip extends StatefulWidget {
|
||||
@@ -66,12 +64,6 @@ class TimeFilterEditor extends StatefulWidget {
|
||||
class _TimeFilterEditorState extends State<TimeFilterEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<TimeFilterEditorBloc, TimeFilterEditorState>(
|
||||
|
||||
+3
-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/field/field_info.dart';
|
||||
@@ -12,6 +10,7 @@ import 'package:flowy_infra_ui/style_widget/scrolling/styled_list.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flowy_infra_ui/widget/spacing.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../../application/field/field_controller.dart';
|
||||
@@ -36,15 +35,15 @@ class GridCreateFilterList extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GridCreateFilterListState extends State<GridCreateFilterList> {
|
||||
late final GridCreateFilterBloc editBloc;
|
||||
late GridCreateFilterBloc editBloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
editBloc = GridCreateFilterBloc(
|
||||
viewId: widget.viewId,
|
||||
fieldController: widget.fieldController,
|
||||
)..add(const GridCreateFilterEvent.initial());
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
+9
-3
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
@@ -7,7 +5,9 @@ import 'package:appflowy/plugins/database/grid/application/filter/filter_menu_bl
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'create_filter_list.dart';
|
||||
@@ -77,7 +77,13 @@ class AddFilterButton extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _AddFilterButtonState extends State<AddFilterButton> {
|
||||
final PopoverController popoverController = PopoverController();
|
||||
late PopoverController popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
popoverController = PopoverController();
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
+3
-3
@@ -1,5 +1,3 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/application/field/field_controller.dart';
|
||||
@@ -11,6 +9,7 @@ import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../layout/sizes.dart';
|
||||
@@ -42,12 +41,13 @@ class GridFieldCell extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _GridFieldCellState extends State<GridFieldCell> {
|
||||
final PopoverController popoverController = PopoverController();
|
||||
late final FieldCellBloc _bloc;
|
||||
late PopoverController popoverController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
popoverController = PopoverController();
|
||||
_bloc = FieldCellBloc(viewId: widget.viewId, fieldInfo: widget.fieldInfo);
|
||||
if (widget.isEditing) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
|
||||
|
||||
+1
-4
@@ -4,7 +4,6 @@ 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';
|
||||
@@ -140,9 +139,7 @@ class _GridHeaderState extends State<_GridHeader> {
|
||||
}
|
||||
|
||||
Widget _cellLeading() {
|
||||
return SizedBox(
|
||||
width: context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
|
||||
);
|
||||
return SizedBox(width: GridSize.horizontalHeaderPadding + 40);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+4
-5
@@ -1,3 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import "package:appflowy/generated/locale_keys.g.dart";
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
|
||||
@@ -5,19 +7,18 @@ 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 {
|
||||
@@ -111,9 +112,7 @@ class _RowLeadingState extends State<_RowLeading> {
|
||||
child: Consumer<RegionStateNotifier>(
|
||||
builder: (context, state, _) {
|
||||
return SizedBox(
|
||||
width: context
|
||||
.read<DatabasePluginWidgetBuilderSize>()
|
||||
.horizontalPadding,
|
||||
width: GridSize.horizontalHeaderPadding + 40,
|
||||
child: state.onEnter ? _activeWidget() : null,
|
||||
);
|
||||
},
|
||||
|
||||
+2
-8
@@ -1,7 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/application/sort/sort_editor_bloc.dart';
|
||||
@@ -9,8 +7,10 @@ import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/sort_entities.pbenum.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import 'create_sort_list.dart';
|
||||
@@ -28,12 +28,6 @@ class SortEditor extends StatefulWidget {
|
||||
class _SortEditorState extends State<SortEditor> {
|
||||
final popoverMutex = PopoverMutex();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
popoverMutex.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<SortEditorBloc, SortEditorState>(
|
||||
|
||||
@@ -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/tab_bar/tab_bar_view.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.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,17 +17,14 @@ 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:
|
||||
context.read<DatabasePluginWidgetBuilderSize>().horizontalPadding,
|
||||
horizontal: GridSize.horizontalHeaderPadding + 40,
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:appflowy/plugins/database/application/database_controller.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/shared/share/share_button.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/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';
|
||||
@@ -17,7 +16,6 @@ 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';
|
||||
@@ -221,16 +219,6 @@ 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,
|
||||
@@ -256,7 +244,6 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
Widget buildWidget({
|
||||
required PluginContext context,
|
||||
required bool shrinkWrap,
|
||||
Map<String, dynamic>? data,
|
||||
}) {
|
||||
notifier.isDeleted.addListener(() {
|
||||
final deletedView = notifier.isDeleted.value;
|
||||
@@ -265,20 +252,11 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
}
|
||||
});
|
||||
|
||||
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,
|
||||
),
|
||||
return DatabaseTabBarView(
|
||||
key: ValueKey(notifier.view.id),
|
||||
view: notifier.view,
|
||||
shrinkWrap: shrinkWrap,
|
||||
initialRowId: initialRowId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -292,7 +270,7 @@ class DatabasePluginWidgetBuilder extends PluginWidgetBuilder {
|
||||
value: bloc,
|
||||
child: Row(
|
||||
children: [
|
||||
ShareButton(key: ValueKey(view.id), view: view),
|
||||
DatabaseShareButton(key: ValueKey(view.id), view: view),
|
||||
const HSpace(10),
|
||||
ViewFavoriteButton(view: view),
|
||||
const HSpace(4),
|
||||
|
||||
+4
-4
@@ -1,8 +1,8 @@
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/row/cells/cell_container.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
|
||||
import 'package:appflowy/plugins/database/application/cell/bloc/select_option_cell_bloc.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_popover/appflowy_popover.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -56,7 +56,7 @@ class DesktopGridSelectOptionCellSkin extends IEditableSelectOptionCellSkin {
|
||||
child: SelectOptionTag(
|
||||
option: option,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 4,
|
||||
vertical: 1,
|
||||
horizontal: 8,
|
||||
),
|
||||
),
|
||||
|
||||
+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';
|
||||
|
||||
Loaded 100 of 1019 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user