Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
05c1924940 | ||
|
|
b2f3f902b2 | ||
|
|
33fbba18ca | ||
|
|
cea3c69239 | ||
|
|
ff91c0a909 | ||
|
|
d1efda4c50 | ||
|
|
2b1d1ba2f4 | ||
|
|
9a237b5f18 | ||
|
|
fc21d1d245 | ||
|
|
0c6d4df14b | ||
|
|
8df5d8dcb4 | ||
|
|
aa9f285f59 | ||
|
|
6173022a15 | ||
|
|
afb479607b | ||
|
|
342c361184 | ||
|
|
790d5612f7 | ||
|
|
99a4e330e8 | ||
|
|
c3b702849f | ||
|
|
a49acd9220 | ||
|
|
dd3b4dc5fb | ||
|
|
155817a0f1 | ||
|
|
89c4629ec2 | ||
|
|
6ca81b3a66 | ||
|
|
b278e14ffd | ||
|
|
cb6f7cb4b6 | ||
|
|
f94feb780a | ||
|
|
0ef1b27ae5 | ||
|
|
64994b3336 | ||
|
|
6966b303ff | ||
|
|
ab8e01bbf7 | ||
|
|
e25633636b | ||
|
|
d87a209c4a | ||
|
|
a33cf1f488 | ||
|
|
91236006d4 | ||
|
|
fd9baf7a06 | ||
|
|
ed646ccba2 | ||
|
|
7bc358d7ac | ||
|
|
14fc287071 | ||
|
|
cdf7dced8b | ||
|
|
552c59218c | ||
|
|
c7e0e36902 | ||
|
|
1f82c6682b | ||
|
|
21261ee813 | ||
|
|
15deb8ea79 | ||
|
|
f7f99a162e | ||
|
|
c05f19edd2 | ||
|
|
407e88b27f | ||
|
|
512113877b | ||
|
|
8f7cb50dd4 | ||
|
|
7dedb84504 | ||
|
|
b2ca5c77f6 | ||
|
|
11d720465b | ||
|
|
c2643bfb6c | ||
|
|
3d4d4cf709 | ||
|
|
b965a5f3ae | ||
|
|
73463cd7e3 | ||
|
|
6158954d97 | ||
|
|
3190eebf6e | ||
|
|
dfe994b341 | ||
|
|
92722d0922 | ||
|
|
e04c1bdaec | ||
|
|
20b16cf174 | ||
|
|
5ffa27f545 | ||
|
|
00cdbe5a1c | ||
|
|
dffb865a66 | ||
|
|
5e581e912f | ||
|
|
256d015967 | ||
|
|
d9b3f3f6c6 | ||
|
|
09fa75f5ec | ||
|
|
38c2937f64 | ||
|
|
06d5bc734b | ||
|
|
a521541cb7 | ||
|
|
7d61252e6a | ||
|
|
cb2b9ac537 | ||
|
|
af31f8cf6f | ||
|
|
87c1245a3f | ||
|
|
49d9417cac | ||
|
|
1f29235538 | ||
|
|
fd1e36b21a | ||
|
|
7db11c7cfd | ||
|
|
33888d583b | ||
|
|
3959cdba3a | ||
|
|
200b367e4c | ||
|
|
956d2dfd07 | ||
|
|
83e50d376e | ||
|
|
802a667907 | ||
|
|
e67e9cc647 | ||
|
|
07c4389f6a | ||
|
|
171c5634f2 | ||
|
|
d7bda10e6f | ||
|
|
20bff9003e | ||
|
|
6e0534400b | ||
|
|
17c116a53b | ||
|
|
27b769362b | ||
|
|
b8e7d57ee6 | ||
|
|
ed052c6792 | ||
|
|
5cd2c63320 | ||
|
|
15567a81e9 | ||
|
|
7ab68dcc2c | ||
|
|
30131fd9e4 | ||
|
|
ddcdd545d9 | ||
|
|
d25a399aba | ||
|
|
ee96a44fef | ||
|
|
dda3962249 | ||
|
|
c2743472bc | ||
|
|
e73fd56152 | ||
|
|
04a013f7ee | ||
|
|
b966e3ea07 | ||
|
|
9d53f758d4 |
No files matched your search
@@ -7,7 +7,6 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -20,7 +19,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.2"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
CARGO_MAKE_VERSION: "0.37.18"
|
||||
CLOUD_VERSION: 0.6.54-amd64
|
||||
|
||||
@@ -40,7 +40,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [ ubuntu-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -74,7 +74,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [windows-latest]
|
||||
os: [ windows-latest ]
|
||||
include:
|
||||
- os: windows-latest
|
||||
flutter_profile: development-windows-x86
|
||||
@@ -101,7 +101,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
os: [macos-latest]
|
||||
os: [ macos-latest ]
|
||||
include:
|
||||
- os: macos-latest
|
||||
flutter_profile: development-mac-x86_64
|
||||
@@ -123,12 +123,12 @@ jobs:
|
||||
flutter_profile: ${{ matrix.flutter_profile }}
|
||||
|
||||
unit_test:
|
||||
needs: [prepare-linux]
|
||||
needs: [ prepare-linux ]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [ ubuntu-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -217,11 +217,11 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
cloud_integration_test:
|
||||
needs: [prepare-linux]
|
||||
needs: [ prepare-linux ]
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
os: [ ubuntu-latest ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
flutter_profile: development-linux-x86_64
|
||||
@@ -340,13 +340,13 @@ jobs:
|
||||
shell: bash
|
||||
|
||||
integration_test:
|
||||
needs: [prepare-linux]
|
||||
needs: [ prepare-linux ]
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
test_number: [1, 2, 3, 4, 5, 6, 7, 8, 9]
|
||||
os: [ ubuntu-latest ]
|
||||
test_number: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
target: "x86_64-unknown-linux-gnu"
|
||||
|
||||
@@ -7,7 +7,6 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
pull_request:
|
||||
@@ -16,12 +15,11 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -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-13, extra-build-args: "" }
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -232,10 +232,10 @@ jobs:
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
targets: "aarch64-apple-darwin,x86_64-apple-darwin",
|
||||
os: macos-latest,
|
||||
extra-build-args: "",
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
@@ -336,12 +336,12 @@ jobs:
|
||||
matrix:
|
||||
job:
|
||||
- {
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
flutter_profile: production-linux-x86_64,
|
||||
}
|
||||
arch: x86_64,
|
||||
target: x86_64-unknown-linux-gnu,
|
||||
os: ubuntu-20.04,
|
||||
extra-build-args: "",
|
||||
flutter_profile: production-linux-x86_64,
|
||||
}
|
||||
steps:
|
||||
- name: Checkout source code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
@@ -19,7 +19,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
CLOUD_VERSION: 0.8.3-amd64
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
ubuntu-job:
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
name: Tauri-CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/tauri2_ci.yaml"
|
||||
- "frontend/rust-lib/**"
|
||||
paths-ignore:
|
||||
- "frontend/appflowy_web_app/**"
|
||||
- "frontend/resources/**"
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
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-ubuntu:
|
||||
runs-on: ubuntu-20.04
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- 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: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_web_app/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install dependencies
|
||||
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
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
shell: bash
|
||||
|
||||
- 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"
|
||||
@@ -1,111 +0,0 @@
|
||||
name: Tauri-CI
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- build/tauri
|
||||
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
tauri-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ubuntu-20.04]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- 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: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
|
||||
|
||||
- name: Node_modules cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: frontend/appflowy_tauri/node_modules
|
||||
key: node-modules-${{ runner.os }}
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force --locked 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
|
||||
|
||||
- name: install cargo-make
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force --locked cargo-make
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
cargo make --cwd .. tauri_build
|
||||
|
||||
- name: frontend tests and linting
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
pnpm test
|
||||
pnpm test:errors
|
||||
|
||||
- uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
args: "--debug"
|
||||
@@ -1,152 +0,0 @@
|
||||
name: Publish Tauri Release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "The branch to release"
|
||||
required: true
|
||||
default: "main"
|
||||
version:
|
||||
description: "The version to release"
|
||||
required: true
|
||||
default: "0.0.0"
|
||||
env:
|
||||
NODE_VERSION: "18.16.0"
|
||||
PNPM_VERSION: "8.5.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
publish-tauri:
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
settings:
|
||||
- platform: windows-latest
|
||||
args: "--verbose"
|
||||
target: "windows-x86_64"
|
||||
- platform: macos-latest
|
||||
args: "--target x86_64-apple-darwin"
|
||||
target: "macos-x86_64"
|
||||
- platform: ubuntu-20.04
|
||||
args: "--target x86_64-unknown-linux-gnu"
|
||||
target: "linux-x86_64"
|
||||
|
||||
runs-on: ${{ matrix.settings.platform }}
|
||||
|
||||
env:
|
||||
CI: true
|
||||
PACKAGE_PREFIX: AppFlowy_Tauri-${{ github.event.inputs.version }}-${{ matrix.settings.target }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch }}
|
||||
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
|
||||
- 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: Install Rust toolchain
|
||||
id: rust_toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ env.RUST_TOOLCHAIN }}
|
||||
override: true
|
||||
profile: minimal
|
||||
|
||||
- name: Rust cache
|
||||
uses: swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: "./frontend/appflowy_tauri/src-tauri -> target"
|
||||
|
||||
- name: install dependencies (windows only)
|
||||
if: matrix.settings.platform == 'windows-latest'
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force --locked duckscript_cli
|
||||
vcpkg integrate install
|
||||
|
||||
- name: install dependencies (ubuntu only)
|
||||
if: matrix.settings.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
|
||||
|
||||
- name: install cargo-make
|
||||
working-directory: frontend
|
||||
run: |
|
||||
cargo install --force --locked cargo-make
|
||||
cargo make appflowy-tauri-deps-tools
|
||||
|
||||
- name: install frontend dependencies
|
||||
working-directory: frontend/appflowy_tauri
|
||||
run: |
|
||||
mkdir dist
|
||||
pnpm install
|
||||
pnpm exec node scripts/update_version.cjs ${{ github.event.inputs.version }}
|
||||
cargo make --cwd .. tauri_build
|
||||
|
||||
- uses: tauri-apps/tauri-action@dev
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.MACOS_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.MACOS_CERTIFICATE_PWD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ secrets.MACOS_TEAM_ID }}
|
||||
APPLE_ID: ${{ secrets.MACOS_NOTARY_USER }}
|
||||
APPLE_TEAM_ID: ${{ secrets.MACOS_TEAM_ID }}
|
||||
APPLE_PASSWORD: ${{ secrets.MACOS_NOTARY_PWD }}
|
||||
CI: true
|
||||
with:
|
||||
args: ${{ matrix.settings.args }}
|
||||
appVersion: ${{ github.event.inputs.version }}
|
||||
tauriScript: pnpm tauri
|
||||
projectPath: frontend/appflowy_tauri
|
||||
|
||||
- name: Upload EXE package(windows only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'windows-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.exe
|
||||
path: frontend/appflowy_tauri/src-tauri/target/release/bundle/nsis/AppFlowy_${{ github.event.inputs.version }}_x64-setup.exe
|
||||
|
||||
- name: Upload DMG package(macos only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'macos-latest'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.dmg
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-apple-darwin/release/bundle/dmg/AppFlowy_${{ github.event.inputs.version }}_x64.dmg
|
||||
|
||||
- name: Upload Deb package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.deb
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/deb/app-flowy_${{ github.event.inputs.version }}_amd64.deb
|
||||
|
||||
- name: Upload AppImage package(ubuntu only)
|
||||
uses: actions/upload-artifact@v4
|
||||
if: matrix.settings.platform == 'ubuntu-20.04'
|
||||
with:
|
||||
name: ${{ env.PACKAGE_PREFIX }}.AppImage
|
||||
path: frontend/appflowy_tauri/src-tauri/target/x86_64-unknown-linux-gnu/release/bundle/appimage/app-flowy_${{ github.event.inputs.version }}_amd64.AppImage
|
||||
@@ -1,75 +0,0 @@
|
||||
name: Web-CI
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/web2_ci.yaml"
|
||||
- "frontend/appflowy_web_app/**"
|
||||
- "frontend/resources/**"
|
||||
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:
|
||||
web-build:
|
||||
if: github.event.pull_request.draft != true
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
platform: [ ubuntu-20.04 ]
|
||||
|
||||
runs-on: ${{ matrix.platform }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-20.04'
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
- 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: Run lint check
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run lint
|
||||
|
||||
- name: build and analyze
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run analyze >> analyze-size.txt
|
||||
- name: Upload analyze-size.txt
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: analyze-size.txt
|
||||
path: frontend/appflowy_web_app/analyze-size.txt
|
||||
retention-days: 30
|
||||
- name: Upload stats.html
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: stats.html
|
||||
path: frontend/appflowy_web_app/dist/stats.html
|
||||
retention-days: 30
|
||||
@@ -1,65 +0,0 @@
|
||||
name: Web Code Coverage
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- ".github/workflows/web2_ci.yaml"
|
||||
- "frontend/appflowy_web_app/**"
|
||||
- "frontend/resources/**"
|
||||
|
||||
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:
|
||||
test:
|
||||
if: github.event.pull_request.draft != true
|
||||
runs-on: ubuntu-22.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Maximize build space (ubuntu only)
|
||||
run: |
|
||||
sudo rm -rf /usr/share/dotnet
|
||||
sudo rm -rf /opt/ghc
|
||||
sudo rm -rf "/usr/local/share/boost"
|
||||
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
|
||||
sudo docker image prune --all --force
|
||||
sudo rm -rf /opt/hostedtoolcache/codeQL
|
||||
sudo rm -rf ${GITHUB_WORKSPACE}/.git
|
||||
sudo rm -rf $ANDROID_HOME/ndk
|
||||
- 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 }}
|
||||
# Install pnpm dependencies, cache them correctly
|
||||
# and run all Cypress tests
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v6
|
||||
with:
|
||||
working-directory: frontend/appflowy_web_app
|
||||
component: true
|
||||
build: pnpm run build
|
||||
start: pnpm run start
|
||||
browser: chrome
|
||||
|
||||
- name: Jest run
|
||||
working-directory: frontend/appflowy_web_app
|
||||
run: |
|
||||
pnpm run test:unit
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
token: cf9245e0-e136-4e21-b0ee-35755fa0c493
|
||||
files: frontend/appflowy_web_app/coverage/jest/lcov.info,frontend/appflowy_web_app/coverage/cypress/lcov.info
|
||||
flags: appflowy_web_app
|
||||
name: frontend/appflowy_web_app
|
||||
fail_ci_if_error: true
|
||||
verbose: true
|
||||
|
||||
+65
-9
@@ -1,17 +1,73 @@
|
||||
# Release Notes
|
||||
## Version 0.7.7 - 09/12/2024
|
||||
## Version 0.8.1 - 14/01/2025
|
||||
### New Features:
|
||||
- AI Chat Layout Options: Customize how AI responses appear with new layouts—List, Table, Image with Text, and Media Only
|
||||
- DALL-E Integration: Generate stunning AI images from text prompts, now available in AI Chat
|
||||
- Improved Desktop Search: Find what you need faster using keywords or by asking questions in natural language
|
||||
- Self-Hosting: Configure web server URLs directly in Settings to enable features like Publish, Copy Link to Share, Custom URLs, and more
|
||||
- Sidebar Enhancement: Drag to reorder your favorited pages in the Sidebar
|
||||
- Mobile Table Resizing: Adjust column widths in Simple Tables by long pressing the column borders on mobile
|
||||
### Bug Fixes
|
||||
- Resolved an icon rendering issue in callout blocks, tab bars, and search results
|
||||
- Enhanced image reliability: Retry functionality ensures images load successfully if the first attempt fails
|
||||
|
||||
## Version 0.8.0 - 06/01/2025
|
||||
### Bug Fixes
|
||||
- Fixed error displaying in the page style menu
|
||||
- Fixed filter logic in the icon picker
|
||||
- Fixed error displaying in the Favorite/Recent page
|
||||
- Fixed the color picker displaying when tapping down
|
||||
- Fixed icons not being supported in subpage blocks
|
||||
- Fixed recent icon functionality in the space icon menu
|
||||
- Fixed "Insert Below" not auto-scrolling the table
|
||||
- Fixed a to-do item with an emoji automatically creating a soft break
|
||||
- Fixed header row/column tap areas being too small
|
||||
- Fixed simple table alignment not working for items that wrap
|
||||
- Fixed web content reverting after removing the inline code format on desktop
|
||||
- Fixed inability to make changes to a row or column in the table when opening a new tab
|
||||
- Fixed changing the language to CKB-KU causing a gray screen on mobile
|
||||
|
||||
## Version 0.7.9 - 30/12/2024
|
||||
### New Features
|
||||
- Added support "set to page width" and "distribute columns evenly" for table block
|
||||
- Added support for reordering column/row in table block
|
||||
- Added support inserting multiple lines in table block
|
||||
- Revamped the mention page interactions in AI chat page
|
||||
- Improve the AI referenced sources UI in AI chat page
|
||||
- Meet AppFlowy Web (Lite): Use AppFlowy directly in your browser.
|
||||
- Create beautiful documents with 22 content types and markdown support
|
||||
- Use Quick Note to save anything you want to remember—like meeting notes, a grocery list, or to-dos
|
||||
- Invite members to your workspace for seamless collaboration
|
||||
- Create multiple public/private spaces to better organize your content
|
||||
- Simple Table is now available on Mobile, designed specifically for mobile devices.
|
||||
- Create and manage Simple Table blocks on Mobile with easy-to-use action menus.
|
||||
- Use the '+' button in the fixed toolbar to easily add a content block into a table cell on Mobile
|
||||
- Use '/' to insert a content block into a table cell on Desktop
|
||||
- Add pages as AI sources in AI chat, enabling you to ask questions about the selected sources
|
||||
- Add messages to an editable document while chatting with AI side by side
|
||||
- The new Emoji menu now includes Icons with a Recent section for quickly reusing emojis/icons
|
||||
- Drag a page from the sidebar into a document to easily mention the page without typing its title
|
||||
- Paste as plain text, a new option in the right-click paste menu
|
||||
### Bug Fixes
|
||||
- Fixed misalignment in numbered lists
|
||||
- Resolved several bugs in the emoji menu
|
||||
- Fixed a bug with checklist items
|
||||
|
||||
## Version 0.7.8 - 18/12/2024
|
||||
### New Features
|
||||
<img width="1068" alt="image" src="https://github.com/user-attachments/assets/cf8bd287-f370-4291-8638-76e2bbf4aaac" />
|
||||
|
||||
- Meet Simple Table 2.0:
|
||||
- Insert a list into a table cell
|
||||
- Insert images, quotes, callouts, and code blocks into a table cell
|
||||
- Drag to move rows or columns
|
||||
- Toggle header rows or columns on/off
|
||||
- Distribute columns evenly
|
||||
- Adjust to page width
|
||||
- Enjoy a new UI/UX for a seamless experience
|
||||
- Revamped mention page interactions in AI Chat
|
||||
- Improved AppFlowy AI service
|
||||
|
||||
### Bug Fixes
|
||||
- Fixed an error when opening files in the database in local mode
|
||||
- Fixed arrow up/down navigation not working for selecting a language in code block
|
||||
- Fixed an issue where deleting multiple blocks using the drag button on the document page didn't work
|
||||
|
||||
- Fixed arrow up/down navigation not working for selecting a language in Code Block
|
||||
- Fixed an issue where deleting multiple blocks using the drag button on the document page didn’t work
|
||||
|
||||
## Version 0.7.7 - 09/12/2024
|
||||
### Bug Fixes
|
||||
- Fixed sidebar menu resize regression
|
||||
|
||||
Vendored
-15
@@ -121,20 +121,5 @@
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
{
|
||||
// https://tauri.app/v1/guides/debugging/vs-code
|
||||
"type": "lldb",
|
||||
"request": "launch",
|
||||
"name": "AF-tauri: Debug backend",
|
||||
"cargo": {
|
||||
"args": [
|
||||
"build",
|
||||
"--manifest-path=./appflowy_tauri/src-tauri/Cargo.toml",
|
||||
"--no-default-features"
|
||||
]
|
||||
},
|
||||
"preLaunchTask": "AF: Tauri UI Dev",
|
||||
"cwd": "${workspaceRoot}/appflowy_tauri/"
|
||||
},
|
||||
]
|
||||
}
|
||||
Vendored
-45
@@ -245,51 +245,6 @@
|
||||
"problemMatcher": [],
|
||||
"detail": "appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri UI Build",
|
||||
"type": "shell",
|
||||
"command": "pnpm run build",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri UI Dev",
|
||||
"type": "shell",
|
||||
"isBackground": true,
|
||||
"command": "pnpm sync:i18n && pnpm run dev",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri Clean",
|
||||
"type": "shell",
|
||||
"command": "cargo make tauri_clean",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri Clean + Dev",
|
||||
"type": "shell",
|
||||
"dependsOrder": "sequence",
|
||||
"dependsOn": [
|
||||
"AF: Tauri Clean",
|
||||
"AF: Tauri UI Dev"
|
||||
],
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Tauri ESLint",
|
||||
"type": "shell",
|
||||
"command": "npx eslint --fix src",
|
||||
"options": {
|
||||
"cwd": "${workspaceFolder}/appflowy_tauri"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "AF: Generate Env File",
|
||||
"type": "shell",
|
||||
|
||||
@@ -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.7.8"
|
||||
APPFLOWY_VERSION = "0.8.1"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -53,7 +53,7 @@ android {
|
||||
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
|
||||
applicationId "io.appflowy.appflowy"
|
||||
minSdkVersion 29
|
||||
targetSdkVersion 34
|
||||
targetSdkVersion 35
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
multiDexEnabled true
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'data_migration/data_migration_test_runner.dart'
|
||||
as data_migration_test_runner;
|
||||
import 'document/document_test_runner.dart' as document_test_runner;
|
||||
import 'set_env.dart' as preset_af_cloud_env_test;
|
||||
import 'sidebar/sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar/sidebar_move_page_test.dart' as sidebar_move_page_test;
|
||||
import 'sidebar/sidebar_rename_untitled_test.dart'
|
||||
as sidebar_rename_untitled_test;
|
||||
@@ -26,4 +27,5 @@ Future<void> main() async {
|
||||
// sidebar
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_more_popup.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../../shared/emoji.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('Change slide bar space icon', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
final firstIcon = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(SidebarSpaceHeader),
|
||||
onHover: () async {
|
||||
final moreOption = find.byType(SpaceMorePopup);
|
||||
await tester.tapButton(moreOption);
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsNothing);
|
||||
await tester.tapSvgButton(SpaceMoreActionType.changeIcon.leftIconSvg);
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final icons = find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
|
||||
);
|
||||
expect(icons, findsOneWidget);
|
||||
await tester.tapIcon(EmojiIconData.icon(firstIcon));
|
||||
|
||||
final spaceHeader = find.byType(SidebarSpaceHeader);
|
||||
final spaceIcon = find.descendant(
|
||||
of: spaceHeader,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
|
||||
),
|
||||
);
|
||||
expect(spaceIcon, findsOneWidget);
|
||||
});
|
||||
}
|
||||
+67
-7
@@ -1,10 +1,11 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -102,8 +103,7 @@ void main() {
|
||||
expect(memberCount, findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('only display one menu item in the workspace menu',
|
||||
(tester) async {
|
||||
testWidgets('workspace menu popover behavior test', (tester) async {
|
||||
// only run the test when the feature flag is on
|
||||
if (!FeatureFlag.collaborativeWorkspace.isOn) {
|
||||
return;
|
||||
@@ -128,6 +128,8 @@ void main() {
|
||||
final workspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name == name,
|
||||
);
|
||||
|
||||
// the workspace menu shouldn't conflict with logout
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
@@ -136,15 +138,73 @@ void main() {
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
final logoutButton = find.byType(WorkspaceMoreButton);
|
||||
await tester.tapButton(logoutButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsOneWidget);
|
||||
expect(moreButton, findsNothing);
|
||||
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_logout.tr()), findsNothing);
|
||||
expect(moreButton, findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more action button for the same workspace shouldn't do
|
||||
// anything
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
// click it again
|
||||
await tester.tapButton(moreButton);
|
||||
|
||||
// nothing should happen
|
||||
expect(
|
||||
find.text(LocaleKeys.button_rename.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
await tester.sendKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// clicking on the more button of another workspace should close the menu
|
||||
// for this one
|
||||
await tester.openCollaborativeWorkspaceMenu();
|
||||
final moreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name == name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
workspaceItem,
|
||||
onHover: () async {
|
||||
expect(moreButton, findsOneWidget);
|
||||
await tester.tapButton(moreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
final otherWorspaceItem = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMenuItem && w.workspace.name != name,
|
||||
);
|
||||
final otherMoreButton = find.byWidgetPredicate(
|
||||
(w) => w is WorkspaceMoreActionList && w.workspace.name != name,
|
||||
);
|
||||
await tester.hoverOnWidget(
|
||||
otherWorspaceItem,
|
||||
onHover: () async {
|
||||
expect(otherMoreButton, findsOneWidget);
|
||||
await tester.tapButton(otherMoreButton);
|
||||
expect(find.text(LocaleKeys.button_rename.tr()), findsOneWidget);
|
||||
|
||||
expect(moreButton, findsNothing);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ void main() {
|
||||
);
|
||||
|
||||
final shareValues = plainText!
|
||||
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
|
||||
.replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')
|
||||
.split('/');
|
||||
final workspaceId = shareValues[0];
|
||||
expect(workspaceId, isNotEmpty);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
|
||||
@@ -71,5 +72,16 @@ void main() {
|
||||
|
||||
expect(find.byType(FlowyTab), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('the space view should not be opened', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
expect(find.byType(AppFlowyEditorPage), findsNothing);
|
||||
expect(find.text('Blank page'), findsOne);
|
||||
});
|
||||
});
|
||||
}
|
||||
+1
-1
@@ -249,7 +249,7 @@ More actions for published page:
|
||||
await tester.openSettings();
|
||||
await tester.openSettingsPage(SettingsPage.sites);
|
||||
// wait the backend return the sites data
|
||||
await tester.wait(1000);
|
||||
await tester.wait(2000);
|
||||
|
||||
// check if the page is published in sites page
|
||||
final pageItem = find.byWidgetPredicate(
|
||||
|
||||
+99
-3
@@ -1,6 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/command_palette.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_field.dart';
|
||||
import 'package:appflowy/workspace/presentation/command_palette/widgets/search_result_tile.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
@@ -8,7 +17,9 @@ import 'package:integration_test/integration_test.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
|
||||
group('Folder Search', () {
|
||||
testWidgets('Search for views', (tester) async {
|
||||
@@ -45,9 +56,94 @@ void main() {
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
|
||||
// The score should be higher for "ViewOne" thus it should be shown first
|
||||
final firstDocumentWidget = tester
|
||||
.widget(find.byType(SearchResultTile).first) as SearchResultTile;
|
||||
final firstDocumentWidget = tester.widget(
|
||||
find.byType(SearchResultTile).first,
|
||||
) as SearchResultTile;
|
||||
expect(firstDocumentWidget.result.data, firstDocument);
|
||||
});
|
||||
|
||||
testWidgets('Displaying icons in search results', (tester) async {
|
||||
final randomValue = Random().nextInt(10000) + 10000;
|
||||
final pageNames = ['First Page-$randomValue', 'Second Page-$randomValue'];
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
|
||||
/// create two pages
|
||||
for (final pageName in pageNames) {
|
||||
await tester.createNewPageWithNameUnderParent(name: pageName);
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: pageName,
|
||||
layout: ViewLayoutPB.Document,
|
||||
icon: emojiIconData,
|
||||
);
|
||||
}
|
||||
|
||||
await tester.toggleCommandPalette();
|
||||
|
||||
/// search for `Page`
|
||||
final searchFieldFinder = find.descendant(
|
||||
of: find.byType(SearchField),
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
await tester.enterText(searchFieldFinder, 'Page-$randomValue');
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 200));
|
||||
expect(find.byType(SearchResultTile), findsNWidgets(2));
|
||||
|
||||
/// check results
|
||||
final svgs = find.descendant(
|
||||
of: find.byType(SearchResultTile),
|
||||
matching: find.byType(FlowySvg),
|
||||
);
|
||||
expect(svgs, findsNWidgets(2));
|
||||
|
||||
final firstSvg = svgs.first.evaluate().first.widget as FlowySvg,
|
||||
lastSvg = svgs.last.evaluate().first.widget as FlowySvg;
|
||||
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
|
||||
/// icon displayed correctly
|
||||
expect(firstSvg.svgString, iconData.svgString);
|
||||
expect(lastSvg.svgString, iconData.svgString);
|
||||
|
||||
testWidgets('select the content in document and search', (tester) async {
|
||||
const firstDocument = ''; // empty document
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(name: firstDocument);
|
||||
await tester.editor.updateSelection(
|
||||
Selection(
|
||||
start: Position(
|
||||
path: [0],
|
||||
),
|
||||
end: Position(
|
||||
path: [0],
|
||||
offset: 10,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.byType(FloatingToolbar),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.toggleCommandPalette();
|
||||
expect(find.byType(CommandPaletteModal), findsOneWidget);
|
||||
|
||||
expect(
|
||||
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(firstDocument),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
+9
-1
@@ -1,4 +1,5 @@
|
||||
import 'package:appflowy/plugins/database/calendar/presentation/calendar_event_editor.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
@@ -9,7 +10,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('calendar', () {
|
||||
testWidgets('update calendar layout', (tester) async {
|
||||
|
||||
+10
-3
@@ -1,12 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -14,7 +14,14 @@ import '../../shared/database_test_op.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid edit field test:', () {
|
||||
testWidgets('rename existing field', (tester) async {
|
||||
|
||||
+17
-3
@@ -5,6 +5,7 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_edi
|
||||
import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/document/document_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -21,7 +22,14 @@ import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('grid row detail page:', () {
|
||||
testWidgets('opens', (tester) async {
|
||||
@@ -386,11 +394,16 @@ void main() {
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 1);
|
||||
tester.assertPhantomChecklistItemContent("");
|
||||
|
||||
await tester.enterText(find.byType(PhantomChecklistItem), 'task 2');
|
||||
await tester.pumpAndSettle();
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.enter);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 500));
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(ChecklistRowDetailCell),
|
||||
onHover: () async {
|
||||
await tester.tapButton(find.byType(ChecklistItemControl));
|
||||
},
|
||||
);
|
||||
|
||||
tester.assertChecklistTaskInEditor(
|
||||
index: 1,
|
||||
@@ -398,6 +411,7 @@ void main() {
|
||||
isChecked: false,
|
||||
);
|
||||
tester.assertPhantomChecklistItemAtIndex(index: 2);
|
||||
tester.assertPhantomChecklistItemContent("");
|
||||
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/icon/icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/callout/callout_block_component.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
testWidgets('callout with emoji icon picker', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final emojiIconData = await tester.loadIcon();
|
||||
|
||||
/// create a new document
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
/// tap the first line of the document
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
/// create callout
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_callout.tr(),
|
||||
);
|
||||
|
||||
/// select an icon
|
||||
final emojiPickerButton = find.descendant(
|
||||
of: find.byType(CalloutBlockComponentWidget),
|
||||
matching: find.byType(EmojiPickerButton),
|
||||
);
|
||||
await tester.tapButton(emojiPickerButton);
|
||||
await tester.tapIcon(emojiIconData);
|
||||
|
||||
/// verification results
|
||||
final iconData = IconsData.fromJson(jsonDecode(emojiIconData.emoji));
|
||||
final iconWidget = find
|
||||
.descendant(
|
||||
of: emojiPickerButton,
|
||||
matching: find.byType(IconWidget),
|
||||
)
|
||||
.evaluate()
|
||||
.first
|
||||
.widget as IconWidget;
|
||||
final iconWidgetData = iconWidget.iconsData;
|
||||
expect(iconWidgetData.svgString, iconData.svgString);
|
||||
expect(iconWidgetData.iconName, iconData.iconName);
|
||||
expect(iconWidgetData.groupName, iconData.groupName);
|
||||
});
|
||||
}
|
||||
+46
-4
@@ -1,7 +1,5 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/block_menu/block_menu_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
|
||||
@@ -11,6 +9,7 @@ import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
@@ -27,10 +26,13 @@ void main() {
|
||||
await tester.pasteContent(
|
||||
plainText: List.generate(lines, (index) => 'line $index').join('\n'),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 3);
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final text =
|
||||
editorState.document.root.children.first.delta!.toPlainText();
|
||||
final textLines = text.split('\n');
|
||||
for (var i = 0; i < lines; i++) {
|
||||
expect(
|
||||
editorState.getNodeAtPath([i])!.delta!.toPlainText(),
|
||||
textLines[i],
|
||||
'line $i',
|
||||
);
|
||||
}
|
||||
@@ -467,6 +469,44 @@ void main() {
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
# I'm h1
|
||||
## I'm h2
|
||||
### I'm h3
|
||||
#### I'm h4
|
||||
##### I'm h5
|
||||
###### I'm h6''';
|
||||
|
||||
testWidgets('paste markdowns', (tester) async {
|
||||
await tester.pasteContent(
|
||||
plainText: testMarkdownText,
|
||||
(editorState) {
|
||||
final children = editorState.document.root.children;
|
||||
expect(children.length, 6);
|
||||
for (int i = 1; i <= children.length; i++) {
|
||||
final text = children[i - 1].delta!.toPlainText();
|
||||
expect(text, 'I\'m h$i');
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste markdowns as plain', (tester) async {
|
||||
await tester.pasteContent(
|
||||
plainText: testMarkdownText,
|
||||
pasteAsPlain: true,
|
||||
(editorState) {
|
||||
final children = editorState.document.root.children;
|
||||
expect(children.length, 6);
|
||||
for (int i = 1; i <= children.length; i++) {
|
||||
final text = children[i - 1].delta!.toPlainText();
|
||||
final expectText = '${'#' * i} I\'m h$i';
|
||||
expect(text, expectText);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
@@ -476,6 +516,7 @@ extension on WidgetTester {
|
||||
String? plainText,
|
||||
String? html,
|
||||
String? inAppJson,
|
||||
bool pasteAsPlain = false,
|
||||
(String, Uint8List?)? image,
|
||||
}) async {
|
||||
await initializeAppFlowy();
|
||||
@@ -502,6 +543,7 @@ extension on WidgetTester {
|
||||
await simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed: Platform.isLinux || Platform.isWindows,
|
||||
isShiftPressed: pasteAsPlain,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await pumpAndSettle();
|
||||
|
||||
+6
-7
@@ -37,12 +37,12 @@ void main() {
|
||||
|
||||
// set clipboard data
|
||||
final data = [
|
||||
"123456\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"1234567\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"12345678\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n"),
|
||||
"123456\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
"1234567\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
"12345678\n\n",
|
||||
...List.generate(100, (_) => "${generateRandomString(50)}\n\n"),
|
||||
].join();
|
||||
await getIt<ClipboardService>().setData(
|
||||
ClipboardServiceData(
|
||||
@@ -155,7 +155,6 @@ void main() {
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(FindAndReplaceMenuWidget), findsNothing);
|
||||
|
||||
},
|
||||
);
|
||||
}
|
||||
+106
@@ -1,4 +1,10 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/view_meta_info.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -31,4 +37,104 @@ void main() {
|
||||
expect(pageFinder, findsNWidgets(1));
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('count title towards word count', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent();
|
||||
|
||||
Finder title = tester.editor.findDocumentTitle('');
|
||||
|
||||
await tester.openMoreViewActions();
|
||||
final viewMetaInfo = find.byType(ViewMetaInfo);
|
||||
expect(viewMetaInfo, findsOneWidget);
|
||||
|
||||
ViewMetaInfo viewMetaInfoWidget =
|
||||
viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
Counters titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
|
||||
expect(titleCounter.charCount, 0);
|
||||
expect(titleCounter.wordCount, 0);
|
||||
|
||||
/// input [str1] within title
|
||||
const str1 = 'Hello',
|
||||
str2 = '$str1 AppFlowy',
|
||||
str3 = '$str2!',
|
||||
str4 = 'Hello world';
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str1);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str1.length);
|
||||
expect(titleCounter.wordCount, 1);
|
||||
|
||||
/// input [str2] within title
|
||||
title = tester.editor.findDocumentTitle(str1);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str2);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str2.length);
|
||||
expect(titleCounter.wordCount, 2);
|
||||
|
||||
/// input [str3] within title
|
||||
title = tester.editor.findDocumentTitle(str2);
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.tapButton(title);
|
||||
await tester.enterText(title, str3);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
expect(titleCounter.charCount, str3.length);
|
||||
expect(titleCounter.wordCount, 2);
|
||||
|
||||
/// input [str4] within document
|
||||
await tester.simulateKeyEvent(LogicalKeyboardKey.escape);
|
||||
await tester.editor
|
||||
.updateSelection(Selection.collapsed(Position(path: [0])));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor
|
||||
.getCurrentEditorState()
|
||||
.insertTextAtCurrentSelection(str4);
|
||||
await tester.pumpAndSettle(const Duration(seconds: 1));
|
||||
await tester.openMoreViewActions();
|
||||
final texts =
|
||||
find.descendant(of: viewMetaInfo, matching: find.byType(FlowyText));
|
||||
expect(texts, findsNWidgets(3));
|
||||
viewMetaInfoWidget = viewMetaInfo.evaluate().first.widget as ViewMetaInfo;
|
||||
titleCounter = viewMetaInfoWidget.titleCounters!;
|
||||
final Counters documentCounters = viewMetaInfoWidget.documentCounters!;
|
||||
final wordCounter = texts.evaluate().elementAt(0).widget as FlowyText,
|
||||
charCounter = texts.evaluate().elementAt(1).widget as FlowyText;
|
||||
final numberFormat = NumberFormat();
|
||||
expect(
|
||||
wordCounter.text,
|
||||
LocaleKeys.moreAction_wordCount.tr(
|
||||
args: [
|
||||
numberFormat
|
||||
.format(titleCounter.wordCount + documentCounters.wordCount)
|
||||
.toString(),
|
||||
],
|
||||
),
|
||||
);
|
||||
expect(
|
||||
charCounter.text,
|
||||
LocaleKeys.moreAction_charCount.tr(
|
||||
args: [
|
||||
numberFormat
|
||||
.format(
|
||||
titleCounter.charCount + documentCounters.charCount,
|
||||
)
|
||||
.toString(),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
+48
-58
@@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/sub_page/sub_page_block_component.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
@@ -11,6 +13,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
// Test cases for the Document SubPageBlock that needs to be covered:
|
||||
@@ -37,7 +40,14 @@ import '../../shared/util.dart';
|
||||
const _defaultPageName = "";
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Document SubPageBlock tests', () {
|
||||
testWidgets('Insert a new SubPageBlock from Slash menu items',
|
||||
@@ -48,11 +58,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
|
||||
findsNWidgets(3),
|
||||
@@ -67,12 +72,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -91,11 +90,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -144,11 +138,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -202,11 +191,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -243,11 +227,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -293,11 +272,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -336,11 +310,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu(true);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -384,11 +353,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
@@ -411,12 +375,6 @@ void main() {
|
||||
await tester.createNewPageWithNameUnderParent(name: 'SubPageBlock');
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.renamePageWithSecondary(_defaultPageName, 'Child page');
|
||||
expect(find.text('Child page'), findsNWidgets(2));
|
||||
|
||||
@@ -437,11 +395,6 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu(true);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(find.byType(SubPageBlockComponent), findsOneWidget);
|
||||
|
||||
final beforeNode = tester.editor.getNodeAtPath([1]);
|
||||
@@ -498,6 +451,43 @@ void main() {
|
||||
|
||||
expect(find.text('Parent'), findsNWidgets(2));
|
||||
});
|
||||
|
||||
testWidgets('Displaying icon of subpage', (tester) async {
|
||||
const firstPage = 'FirstPage';
|
||||
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(name: firstPage);
|
||||
final icon = await tester.loadIcon();
|
||||
|
||||
/// create subpage
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_subPage_name.tr(),
|
||||
offset: 100,
|
||||
);
|
||||
|
||||
/// add icon
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
await tester.editor.tapAddIconButton();
|
||||
await tester.tapIcon(icon);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.openPage(firstPage);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: firstPage,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
/// check if there is a icon in document
|
||||
final iconWidget = find.byWidgetPredicate((w) {
|
||||
if (w is! RawEmojiIconWidget) return false;
|
||||
final iconData = w.emoji.emoji;
|
||||
return iconData == icon.emoji;
|
||||
});
|
||||
expect(iconWidget, findsOneWidget);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+72
-2
@@ -1,17 +1,34 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_util.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.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/emoji.dart';
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('cover image:', () {
|
||||
testWidgets('document cover tests', (tester) async {
|
||||
@@ -51,6 +68,59 @@ void main() {
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
});
|
||||
|
||||
testWidgets('document cover local image tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
|
||||
// Hover over cover toolbar to show 'Add Cover' and 'Add Icon' buttons
|
||||
await tester.editor.hoverOnCoverToolbar();
|
||||
|
||||
// Insert a document cover
|
||||
await tester.editor.tapOnAddCover();
|
||||
tester.expectToSeeDocumentCover(CoverType.asset);
|
||||
|
||||
// Hover over the cover to show the 'Change Cover' and delete buttons
|
||||
await tester.editor.hoverOnCover();
|
||||
tester.expectChangeCoverAndDeleteButton();
|
||||
|
||||
// Change cover to a local image image
|
||||
final imagePath = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final localImagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final imageFile = File(localImagePath)
|
||||
..writeAsBytesSync(imagePath.buffer.asUint8List());
|
||||
|
||||
await tester.editor.hoverOnCover();
|
||||
await tester.editor.tapOnChangeCover();
|
||||
|
||||
final uploadButton = find.findTextInFlowyText(
|
||||
LocaleKeys.document_imageBlock_upload_label.tr(),
|
||||
);
|
||||
await tester.tapButton(uploadButton);
|
||||
|
||||
mockPickFilePaths(paths: [localImagePath]);
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectToSeeDocumentCover(CoverType.file);
|
||||
|
||||
// Remove the cover
|
||||
await tester.editor.hoverOnCover();
|
||||
await tester.editor.tapOnRemoveCover();
|
||||
tester.expectToSeeNoDocumentCover();
|
||||
|
||||
// Test if deleteImageFromLocalStorage(localImagePath) function is called once
|
||||
await tester.pump(kDoubleTapTimeout);
|
||||
expect(deleteImageTestCounter, 1);
|
||||
|
||||
// delete temp files
|
||||
await imageFile.delete();
|
||||
});
|
||||
|
||||
testWidgets('document icon tests', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -147,7 +217,7 @@ void main() {
|
||||
tester.expectViewHasIcon(
|
||||
gettingStarted,
|
||||
ViewLayoutPB.Document,
|
||||
punch,
|
||||
EmojiIconData.emoji(punch),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
-99
@@ -7,19 +7,16 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/image/cust
|
||||
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/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/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;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:run_with_network_images/run_with_network_images.dart';
|
||||
|
||||
import '../../shared/mock/mock_file_picker.dart';
|
||||
import '../../shared/util.dart';
|
||||
@@ -29,58 +26,6 @@ void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('image block in document', () {
|
||||
Future<void> testEmbedImage(WidgetTester tester, String url) 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(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
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);
|
||||
|
||||
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.tapButton(
|
||||
find.descendant(
|
||||
of: find.byType(EmbedImageUrlWidget),
|
||||
matching: find.text(
|
||||
LocaleKeys.document_imageBlock_embedLink_label.tr(),
|
||||
findRichText: true,
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(ResizableImage), findsOneWidget);
|
||||
final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], url);
|
||||
}
|
||||
|
||||
testWidgets('insert an image from local file', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
@@ -131,50 +76,6 @@ void main() {
|
||||
file.deleteSync();
|
||||
});
|
||||
|
||||
testWidgets('insert a gif image from network', (tester) async {
|
||||
await testEmbedImage(
|
||||
tester,
|
||||
'https://www.easygifanimator.net/images/samples/sparkles.gif',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('insert a jpg image from network', (tester) async {
|
||||
await testEmbedImage(
|
||||
tester,
|
||||
'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',
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('insert an image from unsplash', (tester) async {
|
||||
await runWithNetworkImages(() 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(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
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);
|
||||
expect(find.text('Unsplash'), findsOneWidget);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('insert two images from local file at once', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
+15
-1
@@ -147,7 +147,21 @@ void main() {
|
||||
const text = 'Hello World';
|
||||
await tester.ime.insertText(text);
|
||||
|
||||
expect(find.textContaining(text, findRichText: true), findsOneWidget);
|
||||
final inlineText = find.textContaining(text, findRichText: true);
|
||||
expect(inlineText, findsOneWidget);
|
||||
|
||||
// the text should be in the same line with the math equation
|
||||
final inlineMathEquationPosition = tester.getRect(inlineMathEquation);
|
||||
final textPosition = tester.getRect(inlineText);
|
||||
// allow 5px difference
|
||||
expect(
|
||||
(textPosition.top - inlineMathEquationPosition.top).abs(),
|
||||
lessThan(5),
|
||||
);
|
||||
expect(
|
||||
(textPosition.bottom - inlineMathEquationPosition.bottom).abs(),
|
||||
lessThan(5),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+163
@@ -561,6 +561,169 @@ void main() {
|
||||
final node = editorState.document.nodeAtPath([0, 0, 0])!;
|
||||
expect(node.children.length, 1);
|
||||
});
|
||||
|
||||
testWidgets('using option menu to set table align', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(beforeAlign, TableAlign.left);
|
||||
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_center.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign, TableAlign.center);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_right.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign2, TableAlign.right);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_left.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign3, TableAlign.left);
|
||||
});
|
||||
|
||||
testWidgets('using option menu to set table align', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final beforeAlign = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(beforeAlign, TableAlign.left);
|
||||
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_center.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign, TableAlign.center);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_right.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign2 = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign2, TableAlign.right);
|
||||
|
||||
await tester.editor.hoverAndClickOptionMenuButton([0]);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_align.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
LocaleKeys.document_plugins_optionAction_left.tr(),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final afterAlign3 = editorState.document.nodeAtPath([0])!.tableAlign;
|
||||
expect(afterAlign3, TableAlign.left);
|
||||
});
|
||||
|
||||
testWidgets('support slash menu in table', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: 'simple_table_test',
|
||||
);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.insertTableInDocument();
|
||||
|
||||
final path = [0, 0, 0, 0];
|
||||
final selection = Selection.collapsed(Position(path: path));
|
||||
editorState.selection = selection;
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final paragraphItem = find.byWidgetPredicate((w) {
|
||||
return w is SelectionMenuItemWidget &&
|
||||
w.item.name == LocaleKeys.document_slashMenu_name_text.tr();
|
||||
});
|
||||
expect(paragraphItem, findsOneWidget);
|
||||
|
||||
await tester.tap(paragraphItem);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final paragraphNode = editorState.document.nodeAtPath(path)!;
|
||||
expect(paragraphNode.type, equals(ParagraphBlockKeys.type));
|
||||
});
|
||||
}
|
||||
|
||||
extension on WidgetTester {
|
||||
|
||||
+6
-4
@@ -35,10 +35,12 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
),
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
)
|
||||
.first,
|
||||
onHover: () async {
|
||||
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
+21
-8
@@ -2,10 +2,10 @@ import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/desktop_sign_in_screen.dart';
|
||||
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
import 'package:toastification/toastification.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
@@ -23,7 +23,7 @@ void main() {
|
||||
.last;
|
||||
}
|
||||
|
||||
group('sign-in page settings: ', () {
|
||||
group('sign-in page settings:', () {
|
||||
testWidgets('change server type', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
|
||||
@@ -45,28 +45,36 @@ void main() {
|
||||
|
||||
// change the server type to self-host
|
||||
await tester.tapButton(appflowyCloudType);
|
||||
final selfhostedButton = findServerType(
|
||||
final selfHostedButton = findServerType(
|
||||
AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapButton(selfhostedButton);
|
||||
await tester.tapButton(selfHostedButton);
|
||||
|
||||
// update server url
|
||||
const serverUrl = 'https://test.appflowy.cloud';
|
||||
const serverUrl = 'https://self-hosted.appflowy.cloud';
|
||||
await tester.enterText(
|
||||
find.byKey(kSelfHostedTextInputFieldKey),
|
||||
serverUrl,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
// update the web url
|
||||
const webUrl = 'https://self-hosted.appflowy.com';
|
||||
await tester.enterText(
|
||||
find.byKey(kSelfHostedWebTextInputFieldKey),
|
||||
webUrl,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(LocaleKeys.button_save.tr()),
|
||||
);
|
||||
|
||||
// wait the app to restart, and the tooltip to disappear
|
||||
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
|
||||
await tester.pumpUntilNotFound(find.byType(DesktopToast));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// open settings page to check the result
|
||||
await tester.tapButton(settingsButton);
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// check the server type
|
||||
expect(
|
||||
@@ -78,18 +86,23 @@ void main() {
|
||||
find.text(serverUrl),
|
||||
findsOneWidget,
|
||||
);
|
||||
// check the web url
|
||||
expect(
|
||||
find.text(webUrl),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// reset to appflowy cloud
|
||||
await tester.tapButton(
|
||||
findServerType(AuthenticatorType.appflowyCloudSelfHost),
|
||||
);
|
||||
);
|
||||
// change the server type to appflowy cloud
|
||||
await tester.tapButton(
|
||||
findServerType(AuthenticatorType.appflowyCloud),
|
||||
);
|
||||
|
||||
// wait the app to restart, and the tooltip to disappear
|
||||
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
|
||||
await tester.pumpUntilNotFound(find.byType(DesktopToast));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// check the server type
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
@@ -44,5 +48,82 @@ void main() {
|
||||
);
|
||||
expect(isExpanded(type: FolderSpaceType.private), true);
|
||||
});
|
||||
|
||||
testWidgets('Expanding with subpage', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
const page1 = 'SubPageBloc', page2 = '$page1 2';
|
||||
await tester.createNewPageWithNameUnderParent(name: page1);
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: page2,
|
||||
parentName: page1,
|
||||
);
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: gettingStarted,
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
await tester.tapNewPageButton();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final slashMenu = find
|
||||
.ancestor(
|
||||
of: find.byType(SelectionMenuItemWidget),
|
||||
matching: find.byWidgetPredicate(
|
||||
(widget) => widget is Scrollable,
|
||||
),
|
||||
)
|
||||
.first;
|
||||
final slashMenuItem = find.text(
|
||||
LocaleKeys.document_slashMenu_name_linkedDoc.tr(),
|
||||
);
|
||||
await tester.scrollUntilVisible(
|
||||
slashMenuItem,
|
||||
100,
|
||||
scrollable: slashMenu,
|
||||
duration: const Duration(milliseconds: 250),
|
||||
);
|
||||
|
||||
final menuItemFinder = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is SelectionMenuItemWidget &&
|
||||
w.item.name == LocaleKeys.document_slashMenu_name_linkedDoc.tr(),
|
||||
);
|
||||
|
||||
final menuItem =
|
||||
menuItemFinder.evaluate().first.widget as SelectionMenuItemWidget;
|
||||
|
||||
/// tapSlashMenuItemWithName is not working, so invoke this function directly
|
||||
menuItem.item.handler(
|
||||
menuItem.editorState,
|
||||
menuItem.menuService,
|
||||
menuItemFinder.evaluate().first,
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
final actionHandler = find.byType(InlineActionsHandler);
|
||||
final subPage = find.descendant(
|
||||
of: actionHandler,
|
||||
matching: find.text(page2, findRichText: true),
|
||||
);
|
||||
await tester.tapButton(subPage);
|
||||
|
||||
final subpageBlock = find.descendant(
|
||||
of: find.byType(AppFlowyEditor),
|
||||
matching: find.text(page2, findRichText: true),
|
||||
);
|
||||
|
||||
expect(find.text(page2, findRichText: true), findsOneWidget);
|
||||
await tester.tapButton(subpageBlock);
|
||||
|
||||
/// one is in SectionFolder, another one is in CoverTitle
|
||||
/// the last one is in FlowyNavigation
|
||||
expect(find.text(page2, findRichText: true), findsNWidgets(3));
|
||||
});
|
||||
});
|
||||
}
|
||||
+53
@@ -196,5 +196,58 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'reorder favorites',
|
||||
(tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// there are no favorite views
|
||||
final favorites = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.byType(ViewItem),
|
||||
);
|
||||
expect(favorites, findsNothing);
|
||||
|
||||
/// create views and then favorite them
|
||||
const pageNames = ['001', '002', '003'];
|
||||
for (final name in pageNames) {
|
||||
await tester.createNewPageWithNameUnderParent(name: name);
|
||||
}
|
||||
for (final name in pageNames) {
|
||||
await tester.favoriteViewByName(name);
|
||||
}
|
||||
expect(favorites, findsNWidgets(pageNames.length));
|
||||
|
||||
final oldNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(oldNames, pageNames);
|
||||
|
||||
/// drag first to last
|
||||
await tester.reorderFavorite(
|
||||
fromName: '001',
|
||||
toName: '003',
|
||||
);
|
||||
List<String> newNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(newNames, ['002', '003', '001']);
|
||||
|
||||
/// drag first to second
|
||||
await tester.reorderFavorite(
|
||||
fromName: '002',
|
||||
toName: '003',
|
||||
);
|
||||
newNames = favorites
|
||||
.evaluate()
|
||||
.map((e) => (e.widget as ViewItem).view.name)
|
||||
.toList();
|
||||
expect(newNames, ['003', '002', '001']);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
+191
-62
@@ -1,83 +1,212 @@
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/base.dart';
|
||||
import '../../shared/common_operations.dart';
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/expectation.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
final emoji = EmojiIconData.emoji('😁');
|
||||
|
||||
const emoji = '😁';
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
group('Icon:', () {
|
||||
testWidgets('Update page icon in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
testWidgets('Update page emoji in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// update its icon
|
||||
await tester.updatePageIconInSidebarByName(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
icon: emoji,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
testWidgets('Update page icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
// update its emoji
|
||||
await tester.updatePageIconInSidebarByName(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
icon: emoji,
|
||||
);
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
testWidgets('Update page emoji in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// update its icon
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: value.name,
|
||||
layout: value,
|
||||
icon: emoji,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
|
||||
tester.expectViewTitleHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
// update its emoji
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: value.name,
|
||||
layout: value,
|
||||
icon: emoji,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
|
||||
tester.expectViewTitleHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Emoji Search Bar Get Focus', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
await tester.openPage(
|
||||
value.name,
|
||||
layout: value,
|
||||
);
|
||||
final title = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(value.name),
|
||||
);
|
||||
await tester.tapButton(title);
|
||||
await tester.tapButton(find.byType(EmojiPickerButton));
|
||||
|
||||
final emojiPicker = find.byType(FlowyEmojiPicker);
|
||||
expect(emojiPicker, findsOneWidget);
|
||||
final textField = find.descendant(
|
||||
of: emojiPicker,
|
||||
matching: find.byType(FlowyTextField),
|
||||
);
|
||||
expect(textField, findsOneWidget);
|
||||
final textFieldWidget =
|
||||
textField.evaluate().first.widget as FlowyTextField;
|
||||
assert(textFieldWidget.focusNode!.hasFocus);
|
||||
await tester.tapEmoji(emoji.emoji);
|
||||
await tester.pumpAndSettle();
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
emoji,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Update page icon in sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
// update its icon
|
||||
await tester.updatePageIconInSidebarByName(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
icon: iconData,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
iconData,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Update page icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
// create document, board, grid and calendar views
|
||||
for (final value in ViewLayoutPB.values) {
|
||||
if (value == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
name: value.name,
|
||||
parentName: gettingStarted,
|
||||
layout: value,
|
||||
);
|
||||
|
||||
// update its icon
|
||||
await tester.updatePageIconInTitleBarByName(
|
||||
name: value.name,
|
||||
layout: value,
|
||||
icon: iconData,
|
||||
);
|
||||
|
||||
tester.expectViewHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
iconData,
|
||||
);
|
||||
|
||||
tester.expectViewTitleHasIcon(
|
||||
value.name,
|
||||
value,
|
||||
iconData,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../shared/base.dart';
|
||||
import '../../shared/common_operations.dart';
|
||||
import '../../shared/expectation.dart';
|
||||
|
||||
void main() {
|
||||
testWidgets('Skip the empty group name icon in recent icons', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// clear local data
|
||||
RecentIcons.clear();
|
||||
await loadIconGroups();
|
||||
final groups = kIconGroups!;
|
||||
final List<RecentIcon> localIcons = [];
|
||||
for (final e in groups) {
|
||||
localIcons.addAll(e.icons.map((e) => RecentIcon(e, e.name)).toList());
|
||||
}
|
||||
await RecentIcons.putIcon(RecentIcon(localIcons.first.icon, ''));
|
||||
await tester.openPage(gettingStarted);
|
||||
final title = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(gettingStarted),
|
||||
);
|
||||
await tester.tapButton(title);
|
||||
|
||||
/// tap emoji picker button
|
||||
await tester.tapButton(find.byType(EmojiPickerButton));
|
||||
expect(find.byType(FlowyIconEmojiPicker), findsOneWidget);
|
||||
|
||||
/// tap icon tab
|
||||
final pickTab = find.byType(PickerTab);
|
||||
final iconTab = find.descendant(
|
||||
of: pickTab,
|
||||
matching: find.text(PickerTabType.icon.tr),
|
||||
);
|
||||
await tester.tapButton(iconTab);
|
||||
|
||||
expect(find.byType(FlowyIconPicker), findsOneWidget);
|
||||
|
||||
/// no recent icons
|
||||
final recentText = find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
matching: find.text('Recent'),
|
||||
);
|
||||
expect(recentText, findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'sidebar_favorites_test.dart' as sidebar_favorite_test;
|
||||
import 'sidebar_icon_test.dart' as sidebar_icon_test;
|
||||
import 'sidebar_recent_icon_test.dart' as sidebar_recent_icon_test;
|
||||
import 'sidebar_test.dart' as sidebar_test;
|
||||
import 'sidebar_view_item_test.dart' as sidebar_view_item_test;
|
||||
|
||||
@@ -14,4 +15,5 @@ void main() {
|
||||
sidebar_favorite_test.main();
|
||||
sidebar_icon_test.main();
|
||||
sidebar_view_item_test.main();
|
||||
sidebar_recent_icon_test.main();
|
||||
}
|
||||
+15
-2
@@ -1,5 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -11,7 +13,14 @@ import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('Sidebar view item tests', () {
|
||||
testWidgets('Access view item context menu by right click', (tester) async {
|
||||
@@ -38,7 +47,11 @@ void main() {
|
||||
await tester.tapEmoji(emoji);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
tester.expectViewHasIcon(gettingStarted, ViewLayoutPB.Document, emoji);
|
||||
tester.expectViewHasIcon(
|
||||
gettingStarted,
|
||||
ViewLayoutPB.Document,
|
||||
EmojiIconData.emoji(emoji),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+24
-8
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/code_block/code_block_language_selector.dart';
|
||||
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -45,8 +44,10 @@ void main() {
|
||||
await onKey(tester, LogicalKeyboardKey.enter);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final language =
|
||||
editorState.getNodeAtPath([0])!.attributes['language'].toString();
|
||||
String language = editorState
|
||||
.getNodeAtPath([0])!
|
||||
.attributes[CodeBlockKeys.language]
|
||||
.toString();
|
||||
expect(
|
||||
language.toLowerCase(),
|
||||
defaultCodeBlockSupportedLanguages.first.toLowerCase(),
|
||||
@@ -58,14 +59,29 @@ void main() {
|
||||
await onKey(tester, LogicalKeyboardKey.arrowUp);
|
||||
await onKey(tester, LogicalKeyboardKey.enter);
|
||||
|
||||
language = editorState
|
||||
.getNodeAtPath([0])!
|
||||
.attributes[CodeBlockKeys.language]
|
||||
.toString();
|
||||
expect(
|
||||
editorState
|
||||
.getNodeAtPath([0])!
|
||||
.attributes['language']
|
||||
.toString()
|
||||
.toLowerCase(),
|
||||
language.toLowerCase(),
|
||||
defaultCodeBlockSupportedLanguages.last.toLowerCase(),
|
||||
);
|
||||
|
||||
await tester.hoverOnWidget(find.byType(CodeBlockComponentWidget));
|
||||
await tester.tapButtonWithName(language);
|
||||
tester.testTextInput.enterText("rust");
|
||||
await onKey(tester, LogicalKeyboardKey.delete);
|
||||
await onKey(tester, LogicalKeyboardKey.delete);
|
||||
await onKey(tester, LogicalKeyboardKey.arrowDown);
|
||||
tester.testTextInput.enterText("st");
|
||||
await onKey(tester, LogicalKeyboardKey.arrowDown);
|
||||
await onKey(tester, LogicalKeyboardKey.enter);
|
||||
language = editorState
|
||||
.getNodeAtPath([0])!
|
||||
.attributes[CodeBlockKeys.language]
|
||||
.toString();
|
||||
expect(language.toLowerCase(), 'rust');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/flowy_tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/tabs/tabs_manager.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -285,6 +289,39 @@ void main() {
|
||||
expect(tester.isTabAtIndex(secondTabName, 1), isTrue);
|
||||
expect(tester.isTabAtIndex(thirdTabName, 2), isTrue);
|
||||
});
|
||||
|
||||
testWidgets('displaying icons in tab', (tester) async {
|
||||
RecentIcons.enable = false;
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
final icon = await tester.loadIcon();
|
||||
// update emoji
|
||||
await tester.updatePageIconInSidebarByName(
|
||||
name: gettingStarted,
|
||||
parentName: gettingStarted,
|
||||
layout: ViewLayoutPB.Document,
|
||||
icon: icon,
|
||||
);
|
||||
|
||||
/// create new page
|
||||
await tester.createNewPageWithNameUnderParent(name: _documentName);
|
||||
|
||||
/// open new tab for [gettingStarted]
|
||||
await tester.openAppInNewTab(gettingStarted, ViewLayoutPB.Document);
|
||||
|
||||
final tabs = find.descendant(
|
||||
of: find.byType(TabsManager),
|
||||
matching: find.byType(FlowyTab),
|
||||
);
|
||||
expect(tabs, findsNWidgets(2));
|
||||
|
||||
final svgInTab =
|
||||
find.descendant(of: tabs.last, matching: find.byType(FlowySvg));
|
||||
final svgWidget = svgInTab.evaluate().first.widget as FlowySvg;
|
||||
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
|
||||
expect(svgWidget.svgString, iconsData.svgString);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@ import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'page_style_test.dart' as page_style_test;
|
||||
import 'plus_menu_test.dart' as plus_menu_test;
|
||||
import 'simple_table_test.dart' as simple_table_test;
|
||||
import 'title_test.dart' as title_test;
|
||||
import 'toolbar_test.dart' as toolbar_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
@@ -11,4 +13,6 @@ void main() {
|
||||
title_test.main();
|
||||
page_style_test.main();
|
||||
plus_menu_test.main();
|
||||
simple_table_test.main();
|
||||
toolbar_test.main();
|
||||
}
|
||||
@@ -1,17 +1,30 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/cover/document_immersive_cover.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/recent_icons.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
RecentIcons.enable = false;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
RecentIcons.enable = true;
|
||||
});
|
||||
|
||||
group('document page style:', () {
|
||||
double getCurrentEditorFontSize() {
|
||||
@@ -114,5 +127,37 @@ void main() {
|
||||
);
|
||||
expect(builtInCover, findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('page style icon', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
final createPageButton =
|
||||
find.byKey(BottomNavigationBarItemType.add.valueKey);
|
||||
await tester.tapButton(createPageButton);
|
||||
|
||||
/// toggle the preset button
|
||||
await tester.tapSvgButton(FlowySvgs.m_layout_s);
|
||||
|
||||
/// select document plugins emoji
|
||||
final pageStyleIcon = find.byType(PageStyleIcon);
|
||||
|
||||
/// there should be none of emoji
|
||||
final noneText = find.text(LocaleKeys.pageStyle_none.tr());
|
||||
expect(noneText, findsOneWidget);
|
||||
await tester.tapButton(pageStyleIcon);
|
||||
|
||||
/// select an emoji
|
||||
const emoji = '😄';
|
||||
await tester.tapEmoji(emoji);
|
||||
await tester.tapSvgButton(FlowySvgs.m_layout_s);
|
||||
expect(noneText, findsNothing);
|
||||
expect(
|
||||
find.descendant(
|
||||
of: pageStyleIcon,
|
||||
matching: find.text(emoji),
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,554 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('simple table:', () {
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. insert a row above the table
|
||||
3. insert a row below the table
|
||||
4. insert a column left to the table
|
||||
5. insert a column right to the table
|
||||
6. delete the first row
|
||||
7. delete the first column
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final firstParagraphPath = [0, 0, 0, 0];
|
||||
|
||||
// open the plus menu and select the table block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
|
||||
// check the block is inserted
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(2));
|
||||
|
||||
// focus on the first cell
|
||||
|
||||
final selection = editorState.selection!;
|
||||
expect(selection.isCollapsed, isTrue);
|
||||
expect(selection.start.path, equals(firstParagraphPath));
|
||||
}
|
||||
|
||||
// insert left and insert right
|
||||
{
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// insert left, insert right
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_insertLeft.tr(),
|
||||
),
|
||||
);
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_insertRight
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(4));
|
||||
}
|
||||
|
||||
// insert above and insert below
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_insertAbove
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_insertBelow
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.rowLength, equals(4));
|
||||
expect(table.columnLength, equals(4));
|
||||
}
|
||||
|
||||
// delete the first row
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// delete the first row
|
||||
await tester.clickRowMenuButton(0);
|
||||
await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.rowLength, equals(3));
|
||||
expect(table.columnLength, equals(4));
|
||||
}
|
||||
|
||||
// delete the first column
|
||||
{
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.clickColumnMenuButton(0);
|
||||
await tester.clickSimpleTableQuickAction(SimpleTableMoreAction.delete);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.rowLength, equals(3));
|
||||
expect(table.columnLength, equals(3));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. enable header column
|
||||
3. enable header row
|
||||
4. set to page width
|
||||
5. distribute columns evenly
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final firstParagraphPath = [0, 0, 0, 0];
|
||||
|
||||
// open the plus menu and select the table block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
|
||||
// check the block is inserted
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(2));
|
||||
|
||||
// focus on the first cell
|
||||
|
||||
final selection = editorState.selection!;
|
||||
expect(selection.isCollapsed, isTrue);
|
||||
expect(selection.start.path, equals(firstParagraphPath));
|
||||
}
|
||||
|
||||
// enable header column
|
||||
{
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_headerColumn
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// enable header row
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_headerRow.tr(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.isHeaderColumnEnabled, isTrue);
|
||||
expect(table.isHeaderRowEnabled, isTrue);
|
||||
|
||||
// disable header column
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
final toggleButton = find.descendant(
|
||||
of: find.byType(SimpleTableHeaderActionButton),
|
||||
matching: find.byType(CupertinoSwitch),
|
||||
);
|
||||
await tester.tapButton(toggleButton);
|
||||
}
|
||||
|
||||
// enable header row
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
final toggleButton = find.descendant(
|
||||
of: find.byType(SimpleTableHeaderActionButton),
|
||||
matching: find.byType(CupertinoSwitch),
|
||||
);
|
||||
await tester.tapButton(toggleButton);
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
expect(table.isHeaderColumnEnabled, isFalse);
|
||||
expect(table.isHeaderRowEnabled, isFalse);
|
||||
|
||||
// set to page width
|
||||
{
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
final beforeWidth = table.width;
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the row menu button
|
||||
await tester.clickRowMenuButton(0);
|
||||
|
||||
// enable header column
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_setToPageWidth
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
// check the table is updated
|
||||
expect(table.width, greaterThan(beforeWidth));
|
||||
}
|
||||
|
||||
// distribute columns evenly
|
||||
{
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
final beforeWidth = table.width;
|
||||
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// distribute columns evenly
|
||||
await tester.tapButton(
|
||||
find.findTextInFlowyText(
|
||||
LocaleKeys
|
||||
.document_plugins_simpleTable_moreActions_distributeColumnsWidth
|
||||
.tr(),
|
||||
),
|
||||
);
|
||||
|
||||
// check the table is updated
|
||||
expect(table.width, equals(beforeWidth));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. bold
|
||||
3. clear content
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final firstParagraphPath = [0, 0, 0, 0];
|
||||
|
||||
// open the plus menu and select the table block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
|
||||
// check the block is inserted
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(2));
|
||||
|
||||
// focus on the first cell
|
||||
|
||||
final selection = editorState.selection!;
|
||||
expect(selection.isCollapsed, isTrue);
|
||||
expect(selection.start.path, equals(firstParagraphPath));
|
||||
}
|
||||
|
||||
await tester.ime.insertText('Hello');
|
||||
|
||||
// enable bold
|
||||
{
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
// enable bold
|
||||
await tester.clickSimpleTableBoldContentAction();
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the first cell is bold
|
||||
final paragraph = editorState.getNodeAtPath(firstParagraphPath)!;
|
||||
expect(paragraph.isInBoldColumn, isTrue);
|
||||
}
|
||||
|
||||
// clear content
|
||||
{
|
||||
// focus on the first cell
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: firstParagraphPath)),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// click the column menu button
|
||||
await tester.clickColumnMenuButton(0);
|
||||
|
||||
final clearContents = find.findTextInFlowyText(
|
||||
LocaleKeys.document_plugins_simpleTable_moreActions_clearContents
|
||||
.tr(),
|
||||
);
|
||||
|
||||
// clear content
|
||||
final scrollable = find.descendant(
|
||||
of: find.byType(SimpleTableBottomSheet),
|
||||
matching: find.byType(Scrollable),
|
||||
);
|
||||
await tester.scrollUntilVisible(
|
||||
clearContents,
|
||||
100,
|
||||
scrollable: scrollable,
|
||||
);
|
||||
await tester.tapButton(clearContents);
|
||||
await tester.cancelTableActionMenu();
|
||||
|
||||
// check the first cell is empty
|
||||
final paragraph = editorState.getNodeAtPath(firstParagraphPath)!;
|
||||
expect(paragraph.delta!, isEmpty);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. insert a heading block in table cell
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final firstParagraphPath = [0, 0, 0, 0];
|
||||
|
||||
// open the plus menu and select the table block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.document_slashMenu_name_table.tr(),
|
||||
);
|
||||
|
||||
// check the block is inserted
|
||||
final table = editorState.getNodeAtPath([0])!;
|
||||
expect(table.type, equals(SimpleTableBlockKeys.type));
|
||||
expect(table.rowLength, equals(2));
|
||||
expect(table.columnLength, equals(2));
|
||||
|
||||
// focus on the first cell
|
||||
|
||||
final selection = editorState.selection!;
|
||||
expect(selection.isCollapsed, isTrue);
|
||||
expect(selection.start.path, equals(firstParagraphPath));
|
||||
}
|
||||
|
||||
// open the plus menu and select the heading block
|
||||
{
|
||||
await tester.openPlusMenuAndClickButton(
|
||||
LocaleKeys.editor_heading1.tr(),
|
||||
);
|
||||
|
||||
// check the heading block is inserted
|
||||
final heading = editorState.getNodeAtPath([0, 0, 0, 0])!;
|
||||
expect(heading.type, equals(HeadingBlockKeys.type));
|
||||
expect(heading.level, equals(1));
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('''
|
||||
1. insert a simple table via + menu
|
||||
2. resize column
|
||||
''', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
await tester.createNewDocumentOnMobile('simple table');
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
unawaited(
|
||||
editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: [0])),
|
||||
reason: SelectionUpdateReason.uiEvent,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final beforeWidth = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
|
||||
|
||||
// find the first cell
|
||||
{
|
||||
final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;
|
||||
final offset = tester.getCenter(resizeHandle);
|
||||
final gesture = await tester.startGesture(offset, pointer: 7);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.moveBy(const Offset(100, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
final afterWidth1 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
|
||||
expect(afterWidth1, greaterThan(beforeWidth));
|
||||
|
||||
// resize back to the original width
|
||||
{
|
||||
final resizeHandle = find.byType(SimpleTableColumnResizeHandle).first;
|
||||
final offset = tester.getCenter(resizeHandle);
|
||||
final gesture = await tester.startGesture(offset, pointer: 7);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.moveBy(const Offset(-100, 0));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
// check the table is updated
|
||||
final afterWidth2 = editorState.getNodeAtPath([0, 0, 0])!.columnWidth;
|
||||
expect(afterWidth2, equals(beforeWidth));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/editor/mobile_editor_screen.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/appflowy_mobile_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/util.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text_field.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('toolbar menu:', () {
|
||||
testWidgets('insert links', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
final createPageButton = find.byKey(
|
||||
BottomNavigationBarItemType.add.valueKey,
|
||||
);
|
||||
await tester.tapButton(createPageButton);
|
||||
expect(find.byType(MobileDocumentScreen), findsOneWidget);
|
||||
|
||||
final editor = find.byType(AppFlowyEditor);
|
||||
expect(editor, findsOneWidget);
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
|
||||
/// move cursor to content
|
||||
final root = editorState.document.root;
|
||||
final lastNode = root.children.lastOrNull;
|
||||
await editorState.updateSelectionWithReason(
|
||||
Selection.collapsed(Position(path: lastNode!.path)),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// insert two lines of text
|
||||
const strFirst = 'FirstLine',
|
||||
strSecond = 'SecondLine',
|
||||
link = 'google.com';
|
||||
await editorState.insertTextAtCurrentSelection(strFirst);
|
||||
await tester.pumpAndSettle();
|
||||
await editorState.insertNewLine();
|
||||
await tester.pumpAndSettle();
|
||||
await editorState.insertTextAtCurrentSelection(strSecond);
|
||||
await tester.pumpAndSettle();
|
||||
final firstLine = find.text(strFirst, findRichText: true),
|
||||
secondLine = find.text(strSecond, findRichText: true);
|
||||
expect(firstLine, findsOneWidget);
|
||||
expect(secondLine, findsOneWidget);
|
||||
|
||||
/// select the first line
|
||||
await tester.longPress(firstLine);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// find aa item and tap it
|
||||
final aaItem = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is AppFlowyMobileToolbarIconItem &&
|
||||
widget.icon == FlowySvgs.m_toolbar_aa_m,
|
||||
);
|
||||
expect(aaItem, findsOneWidget);
|
||||
await tester.tapButton(aaItem);
|
||||
|
||||
/// find link button and tap it
|
||||
final linkButton = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is MobileToolbarMenuItemWrapper &&
|
||||
widget.icon == FlowySvgs.m_toolbar_link_m,
|
||||
);
|
||||
expect(linkButton, findsOneWidget);
|
||||
await tester.tapButton(linkButton);
|
||||
|
||||
/// input the link
|
||||
final linkField = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is FlowyTextField &&
|
||||
w.hintText == LocaleKeys.document_inlineLink_url_placeholder.tr(),
|
||||
);
|
||||
await tester.enterText(linkField, link);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// complete inputting
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
/// do it again
|
||||
/// select the second line
|
||||
await tester.longPress(secondLine);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(aaItem);
|
||||
await tester.tapButton(linkButton);
|
||||
await tester.enterText(linkField, link);
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tapButton(find.text(LocaleKeys.button_done.tr()));
|
||||
|
||||
final firstNode = editorState.getNodeAtPath([0]);
|
||||
final secondNode = editorState.getNodeAtPath([1]);
|
||||
|
||||
Map commonDeltaJson(String insert) => {
|
||||
"insert": insert,
|
||||
"attributes": {"href": link},
|
||||
};
|
||||
|
||||
expect(
|
||||
firstNode?.delta?.toJson(),
|
||||
commonDeltaJson(strFirst),
|
||||
);
|
||||
expect(
|
||||
secondNode?.delta?.toJson(),
|
||||
commonDeltaJson(strSecond),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';
|
||||
import 'package:appflowy/mobile/presentation/mobile_bottom_navigation_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_option_tile.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('Change default text direction', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
/// tap [Setting] button
|
||||
await tester.tapButton(find.byType(HomePageSettingsPopupMenu));
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.settings_popupMenuItem_settings.tr()));
|
||||
|
||||
/// tap [Default Text Direction]
|
||||
await tester.tapButton(
|
||||
find.text(LocaleKeys.settings_appearance_textDirection_label.tr()),
|
||||
);
|
||||
|
||||
/// there are 3 items: LTR-RTL-AUTO
|
||||
final bottomSheet = find.ancestor(
|
||||
of: find.byType(FlowyOptionTile),
|
||||
matching: find.byType(SafeArea),
|
||||
);
|
||||
final items = find.descendant(
|
||||
of: bottomSheet,
|
||||
matching: find.byType(FlowyOptionTile),
|
||||
);
|
||||
expect(items, findsNWidgets(3));
|
||||
|
||||
/// select [Auto]
|
||||
await tester.tapButton(items.last);
|
||||
expect(
|
||||
find.text(LocaleKeys.settings_appearance_textDirection_auto.tr()),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
/// go back home
|
||||
await tester.tapButton(find.byType(AppBarImmersiveBackButton));
|
||||
|
||||
/// create new page
|
||||
final createPageButton =
|
||||
find.byKey(BottomNavigationBarItemType.add.valueKey);
|
||||
await tester.tapButton(createPageButton);
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
// focus on the editor
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
const testEnglish = 'English', testArabic = 'إنجليزي';
|
||||
|
||||
/// insert [testEnglish]
|
||||
await editorState.insertTextAtCurrentSelection(testEnglish);
|
||||
await tester.pumpAndSettle();
|
||||
await editorState.insertNewLine(position: editorState.selection!.end);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
/// insert [testArabic]
|
||||
await editorState.insertTextAtCurrentSelection(testArabic);
|
||||
await tester.pumpAndSettle();
|
||||
final testEnglishFinder = find.text(testEnglish, findRichText: true),
|
||||
testArabicFinder = find.text(testArabic, findRichText: true);
|
||||
final testEnglishRenderBox =
|
||||
testEnglishFinder.evaluate().first.renderObject as RenderBox,
|
||||
testArabicRenderBox =
|
||||
testArabicFinder.evaluate().first.renderObject as RenderBox;
|
||||
final englishPosition = testEnglishRenderBox.localToGlobal(Offset.zero),
|
||||
arabicPosition = testArabicRenderBox.localToGlobal(Offset.zero);
|
||||
expect(englishPosition.dx > arabicPosition.dx, true);
|
||||
});
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'mobile/document/document_test_runner.dart' as document_test_runner;
|
||||
import 'mobile/home_page/create_new_page_test.dart' as create_new_page_test;
|
||||
import 'mobile/settings/default_text_direction_test.dart'
|
||||
as default_text_direction_test;
|
||||
import 'mobile/sign_in/anonymous_sign_in_test.dart' as anonymous_sign_in_test;
|
||||
|
||||
Future<void> main() async {
|
||||
@@ -17,4 +19,5 @@ Future<void> runIntegration1OnMobile() async {
|
||||
anonymous_sign_in_test.main();
|
||||
create_new_page_test.main();
|
||||
document_test_runner.main();
|
||||
default_text_direction_test.main();
|
||||
}
|
||||
@@ -175,6 +175,33 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapDown(
|
||||
Finder finder, {
|
||||
int? pointer,
|
||||
int buttons = kPrimaryButton,
|
||||
PointerDeviceKind kind = PointerDeviceKind.touch,
|
||||
bool pumpAndSettle = true,
|
||||
int milliseconds = 500,
|
||||
}) async {
|
||||
final location = getCenter(finder);
|
||||
final TestGesture gesture = await startGesture(
|
||||
location,
|
||||
pointer: pointer,
|
||||
buttons: buttons,
|
||||
kind: kind,
|
||||
);
|
||||
await gesture.cancel();
|
||||
await gesture.down(location);
|
||||
await gesture.cancel();
|
||||
if (pumpAndSettle) {
|
||||
await this.pumpAndSettle(
|
||||
Duration(milliseconds: milliseconds),
|
||||
EnginePhase.sendSemanticsUpdate,
|
||||
const Duration(seconds: 15),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> tapButtonWithName(
|
||||
String tr, {
|
||||
int milliseconds = 500,
|
||||
|
||||
@@ -8,18 +8,24 @@ import 'package:appflowy/mobile/presentation/base/type_option_menu_item.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/base/emoji_picker_button.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/mobile_toolbar_v3/add_block_toolbar_item.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/simple_table/simple_table_widgets/_simple_table_bottom_sheet_actions.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_button.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/text_field/text_filed_with_metric_lines.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/presentation/screens/screens.dart';
|
||||
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/favorites/favorite_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/footer/sidebar_footer.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_new_page_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/shared_widget.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_header.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/sidebar_space_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/sidebar_workspace.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/draggable_view_item.dart';
|
||||
@@ -595,6 +601,23 @@ extension CommonOperations on WidgetTester {
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> reorderFavorite({
|
||||
required String fromName,
|
||||
required String toName,
|
||||
}) async {
|
||||
final from = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.text(fromName),
|
||||
),
|
||||
to = find.descendant(
|
||||
of: find.byType(FavoriteFolder),
|
||||
matching: find.text(toName),
|
||||
);
|
||||
final distanceY = getCenter(to).dy - getCenter(from).dx;
|
||||
await drag(from, Offset(0, distanceY));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
// tap the button with [FlowySvgData]
|
||||
Future<void> tapButtonWithFlowySvgData(FlowySvgData svg) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
@@ -606,9 +629,9 @@ extension CommonOperations on WidgetTester {
|
||||
// update the page icon in the sidebar
|
||||
Future<void> updatePageIconInSidebarByName({
|
||||
required String name,
|
||||
required String parentName,
|
||||
String? parentName,
|
||||
required ViewLayoutPB layout,
|
||||
required String icon,
|
||||
required EmojiIconData icon,
|
||||
}) async {
|
||||
final iconButton = find.descendant(
|
||||
of: findPageName(
|
||||
@@ -620,7 +643,11 @@ extension CommonOperations on WidgetTester {
|
||||
find.byTooltip(LocaleKeys.document_plugins_cover_changeIcon.tr()),
|
||||
);
|
||||
await tapButton(iconButton);
|
||||
await tapEmoji(icon);
|
||||
if (icon.type == FlowyIconType.emoji) {
|
||||
await tapEmoji(icon.emoji);
|
||||
} else if (icon.type == FlowyIconType.icon) {
|
||||
await tapIcon(icon);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -628,7 +655,7 @@ extension CommonOperations on WidgetTester {
|
||||
Future<void> updatePageIconInTitleBarByName({
|
||||
required String name,
|
||||
required ViewLayoutPB layout,
|
||||
required String icon,
|
||||
required EmojiIconData icon,
|
||||
}) async {
|
||||
await openPage(
|
||||
name,
|
||||
@@ -640,7 +667,11 @@ extension CommonOperations on WidgetTester {
|
||||
);
|
||||
await tapButton(title);
|
||||
await tapButton(find.byType(EmojiPickerButton));
|
||||
await tapEmoji(icon);
|
||||
if (icon.type == FlowyIconType.emoji) {
|
||||
await tapEmoji(icon.emoji);
|
||||
} else if (icon.type == FlowyIconType.icon) {
|
||||
await tapIcon(icon);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
|
||||
@@ -838,6 +869,70 @@ extension CommonOperations on WidgetTester {
|
||||
await tapButton(toggleHeading1);
|
||||
await pumpUntilNotFound(addMenuItem);
|
||||
}
|
||||
|
||||
/// Click the column menu button in the simple table
|
||||
Future<void> clickColumnMenuButton(int index) async {
|
||||
final columnMenuButton = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is SimpleTableMobileReorderButton &&
|
||||
w.index == index &&
|
||||
w.type == SimpleTableMoreActionType.column,
|
||||
);
|
||||
await tapButton(columnMenuButton);
|
||||
await pumpUntilFound(find.byType(SimpleTableCellBottomSheet));
|
||||
}
|
||||
|
||||
/// Click the row menu button in the simple table
|
||||
Future<void> clickRowMenuButton(int index) async {
|
||||
final rowMenuButton = find.byWidgetPredicate(
|
||||
(w) =>
|
||||
w is SimpleTableMobileReorderButton &&
|
||||
w.index == index &&
|
||||
w.type == SimpleTableMoreActionType.row,
|
||||
);
|
||||
await tapButton(rowMenuButton);
|
||||
await pumpUntilFound(find.byType(SimpleTableCellBottomSheet));
|
||||
}
|
||||
|
||||
/// Click the SimpleTableQuickAction
|
||||
Future<void> clickSimpleTableQuickAction(SimpleTableMoreAction action) async {
|
||||
final button = find.byWidgetPredicate(
|
||||
(widget) => widget is SimpleTableQuickAction && widget.type == action,
|
||||
);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Click the SimpleTableContentAction
|
||||
Future<void> clickSimpleTableBoldContentAction() async {
|
||||
final button = find.byType(SimpleTableContentBoldAction);
|
||||
await tapButton(button);
|
||||
}
|
||||
|
||||
/// Cancel the table action menu
|
||||
Future<void> cancelTableActionMenu() async {
|
||||
final finder = find.byType(SimpleTableCellBottomSheet);
|
||||
if (finder.evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
await tapAt(Offset.zero);
|
||||
await pumpUntilNotFound(finder);
|
||||
}
|
||||
|
||||
/// load icon list and return the first one
|
||||
Future<EmojiIconData> loadIcon() async {
|
||||
await loadIconGroups();
|
||||
final groups = kIconGroups!;
|
||||
final firstGroup = groups.first;
|
||||
final firstIcon = firstGroup.icons.first;
|
||||
return EmojiIconData.icon(
|
||||
IconsData(
|
||||
firstGroup.name,
|
||||
firstIcon.name,
|
||||
builtInSpaceColors.first,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension SettingsFinder on CommonFinders {
|
||||
|
||||
@@ -567,6 +567,12 @@ extension AppFlowyDatabaseTest on WidgetTester {
|
||||
expect(phantom is PhantomChecklistItem, true);
|
||||
}
|
||||
|
||||
void assertPhantomChecklistItemContent(String content) {
|
||||
final phantom = find.byType(PhantomChecklistItem);
|
||||
final text = find.text(content);
|
||||
expect(find.descendant(of: phantom, matching: text), findsOneWidget);
|
||||
}
|
||||
|
||||
Future<void> openFirstRowDetailPage() async {
|
||||
await hoverOnFirstRowOfGrid();
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_color_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon_popup.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
@@ -11,4 +20,43 @@ extension EmojiTestExtension on WidgetTester {
|
||||
);
|
||||
await tapButton(emojiWidget);
|
||||
}
|
||||
|
||||
Future<void> tapIcon(EmojiIconData icon) async {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
|
||||
final pickTab = find.byType(PickerTab);
|
||||
expect(pickTab, findsOneWidget);
|
||||
await pumpAndSettle();
|
||||
final iconTab = find.descendant(
|
||||
of: pickTab,
|
||||
matching: find.text(PickerTabType.icon.tr),
|
||||
);
|
||||
expect(iconTab, findsOneWidget);
|
||||
await tapButton(iconTab);
|
||||
final selectedSvg = find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
),
|
||||
);
|
||||
|
||||
await tapButton(selectedSvg.first);
|
||||
final colorPicker = find.byType(IconColorPicker);
|
||||
expect(colorPicker, findsOneWidget);
|
||||
final selectedColor = find.descendant(
|
||||
of: colorPicker,
|
||||
matching: find.byWidgetPredicate((w) {
|
||||
if (w is Container) {
|
||||
final d = w.decoration;
|
||||
if (d is ShapeDecoration) {
|
||||
if (d.color ==
|
||||
Color(int.parse(iconsData.color ?? builtInSpaceColors.first))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
await tapButton(selectedColor);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
@@ -5,6 +7,8 @@ import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/banner.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_cover_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/icon_picker.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
@@ -14,6 +18,7 @@ import 'package:appflowy/workspace/presentation/widgets/view_title_bar.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';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
@@ -117,7 +122,7 @@ extension Expectation on WidgetTester {
|
||||
return;
|
||||
}
|
||||
final iconWidget = find.byWidgetPredicate(
|
||||
(widget) => widget is EmojiIconWidget && widget.emoji == emoji,
|
||||
(widget) => widget is EmojiIconWidget && widget.emoji.emoji == emoji,
|
||||
);
|
||||
expect(iconWidget, findsOneWidget);
|
||||
}
|
||||
@@ -223,24 +228,52 @@ extension Expectation on WidgetTester {
|
||||
);
|
||||
}
|
||||
|
||||
void expectViewHasIcon(String name, ViewLayoutPB layout, String emoji) {
|
||||
void expectViewHasIcon(String name, ViewLayoutPB layout, EmojiIconData data) {
|
||||
final pageName = findPageName(
|
||||
name,
|
||||
layout: layout,
|
||||
);
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.text(emoji),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
final type = data.type;
|
||||
if (type == FlowyIconType.emoji) {
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.text(data.emoji),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
} else if (type == FlowyIconType.icon) {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void expectViewTitleHasIcon(String name, ViewLayoutPB layout, String emoji) {
|
||||
final icon = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(emoji),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
void expectViewTitleHasIcon(
|
||||
String name,
|
||||
ViewLayoutPB layout,
|
||||
EmojiIconData data,
|
||||
) {
|
||||
final type = data.type;
|
||||
if (type == FlowyIconType.emoji) {
|
||||
final icon = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.text(data.emoji),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
} else if (type == FlowyIconType.icon) {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(data.emoji));
|
||||
final icon = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
}
|
||||
}
|
||||
|
||||
void expectSelectedReminder(ReminderOption option) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy/ai/error.dart';
|
||||
import 'package:appflowy/ai/openai_client.dart';
|
||||
import 'package:appflowy/ai/text_completion.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
|
||||
@@ -88,6 +88,8 @@ PODS:
|
||||
- Toast (4.0.0)
|
||||
- url_launcher_ios (0.0.1):
|
||||
- Flutter
|
||||
- webview_flutter_wkwebview (0.0.1):
|
||||
- Flutter
|
||||
|
||||
DEPENDENCIES:
|
||||
- app_links (from `.symlinks/plugins/app_links/ios`)
|
||||
@@ -112,6 +114,7 @@ DEPENDENCIES:
|
||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||
- super_native_extensions (from `.symlinks/plugins/super_native_extensions/ios`)
|
||||
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
|
||||
- webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`)
|
||||
|
||||
SPEC REPOS:
|
||||
trunk:
|
||||
@@ -168,6 +171,8 @@ EXTERNAL SOURCES:
|
||||
:path: ".symlinks/plugins/super_native_extensions/ios"
|
||||
url_launcher_ios:
|
||||
:path: ".symlinks/plugins/url_launcher_ios/ios"
|
||||
webview_flutter_wkwebview:
|
||||
:path: ".symlinks/plugins/webview_flutter_wkwebview/ios"
|
||||
|
||||
SPEC CHECKSUMS:
|
||||
app_links: e70ca16b4b0f88253b3b3660200d4a10b4ea9795
|
||||
@@ -185,21 +190,22 @@ SPEC CHECKSUMS:
|
||||
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
|
||||
keyboard_height_plugin: 43fa8bba20fd5c4fdeed5076466b8b9d43cc6b86
|
||||
open_filex: 6e26e659846ec990262224a12ef1c528bb4edbe4
|
||||
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
|
||||
package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4
|
||||
path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
|
||||
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
|
||||
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
|
||||
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
|
||||
Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
|
||||
sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
|
||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||
share_plus: 8b6f8b3447e494cca5317c8c3073de39b3600d1f
|
||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||
super_native_extensions: 4916b3c627a9c7fffdc48a23a9eca0b1ac228fa7
|
||||
SwiftyGif: 6c3eafd0ce693cad58bb63d2b2fb9bacb8552780
|
||||
Toast: 91b396c56ee72a5790816f40d3a94dd357abc196
|
||||
url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe
|
||||
webview_flutter_wkwebview: 2a23822e9039b7b1bc52e5add778e5d89ad488d1
|
||||
|
||||
PODFILE CHECKSUM: d0d9b4ff572d8695c38eb3f9b490f55cdfc57eca
|
||||
|
||||
COCOAPODS: 1.15.2
|
||||
COCOAPODS: 1.16.2
|
||||
+5
-3
@@ -1,8 +1,9 @@
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
|
||||
import 'error.dart';
|
||||
import 'text_completion.dart';
|
||||
|
||||
abstract class AIRepository {
|
||||
Future<void> getStreamedCompletions({
|
||||
required String prompt,
|
||||
@@ -17,6 +18,7 @@ abstract class AIRepository {
|
||||
});
|
||||
|
||||
Future<void> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
+12
-3
@@ -3,9 +3,6 @@ import 'dart:ffi';
|
||||
import 'dart:isolate';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/ask_ai_action.dart';
|
||||
import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
@@ -13,6 +10,10 @@ import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:fixnum/fixnum.dart' as fixnum;
|
||||
|
||||
import 'ai_client.dart';
|
||||
import 'error.dart';
|
||||
import 'text_completion.dart';
|
||||
|
||||
class AppFlowyAIService implements AIRepository {
|
||||
@override
|
||||
Future<FlowyResult<List<String>, AIError>> generateImage({
|
||||
@@ -39,6 +40,7 @@ class AppFlowyAIService implements AIRepository {
|
||||
|
||||
@override
|
||||
Future<CompletionStream> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
@@ -52,10 +54,17 @@ class AppFlowyAIService implements AIRepository {
|
||||
onEnd,
|
||||
onError,
|
||||
);
|
||||
final List<String> ragIds = [];
|
||||
if (objectId != null) {
|
||||
ragIds.add(objectId);
|
||||
}
|
||||
|
||||
final payload = CompleteTextPB(
|
||||
text: text,
|
||||
completionType: completionType,
|
||||
streamPort: fixnum.Int64(stream.nativePort),
|
||||
objectId: objectId ?? "",
|
||||
ragIds: ragIds,
|
||||
);
|
||||
|
||||
// ignore: unawaited_futures
|
||||
File renamed without changes.
+2
-1
@@ -1,11 +1,11 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
|
||||
import 'ai_client.dart';
|
||||
import 'error.dart';
|
||||
import 'text_completion.dart';
|
||||
|
||||
@@ -160,6 +160,7 @@ class HttpOpenAIRepository implements AIRepository {
|
||||
|
||||
@override
|
||||
Future<void> streamCompletion({
|
||||
String? objectId,
|
||||
required String text,
|
||||
required CompletionTypePB completionType,
|
||||
required Future<void> Function() onStart,
|
||||
+1
@@ -1,4 +1,5 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'text_completion.freezed.dart';
|
||||
part 'text_completion.g.dart';
|
||||
|
||||
@@ -58,6 +58,7 @@ class KVKeys {
|
||||
|
||||
static const String kCloudType = 'kCloudType';
|
||||
static const String kAppflowyCloudBaseURL = 'kAppFlowyCloudBaseURL';
|
||||
static const String kAppFlowyBaseShareDomain = 'kAppFlowyBaseShareDomain';
|
||||
static const String kAppFlowyEnableSyncTrace = 'kAppFlowyEnableSyncTrace';
|
||||
|
||||
/// The key for saving the text scale factor.
|
||||
@@ -109,4 +110,9 @@ class KVKeys {
|
||||
///
|
||||
/// The value is a boolean string
|
||||
static const String hasUpgradedSpace = 'hasUpgradedSpace060';
|
||||
|
||||
/// The key for saving the recent icons
|
||||
///
|
||||
/// The value is a json string of [RecentIcons]
|
||||
static const String recentIcons = 'kRecentIcons';
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
// ignore_for_file: non_constant_identifier_names
|
||||
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'backend_env.g.dart';
|
||||
|
||||
@JsonSerializable()
|
||||
@@ -40,6 +42,7 @@ class AppFlowyCloudConfiguration {
|
||||
required this.ws_base_url,
|
||||
required this.gotrue_url,
|
||||
required this.enable_sync_trace,
|
||||
required this.base_web_domain,
|
||||
});
|
||||
|
||||
factory AppFlowyCloudConfiguration.fromJson(Map<String, dynamic> json) =>
|
||||
@@ -50,6 +53,13 @@ class AppFlowyCloudConfiguration {
|
||||
final String gotrue_url;
|
||||
final bool enable_sync_trace;
|
||||
|
||||
/// The base domain is used in
|
||||
///
|
||||
/// - Share URL
|
||||
/// - Publish URL
|
||||
/// - Copy Link To Block
|
||||
final String base_web_domain;
|
||||
|
||||
Map<String, dynamic> toJson() => _$AppFlowyCloudConfigurationToJson(this);
|
||||
|
||||
static AppFlowyCloudConfiguration defaultConfig() {
|
||||
@@ -58,6 +68,7 @@ class AppFlowyCloudConfiguration {
|
||||
ws_base_url: '',
|
||||
gotrue_url: '',
|
||||
enable_sync_trace: false,
|
||||
base_web_domain: ShareConstants.defaultBaseWebDomain,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+27
-1
@@ -2,6 +2,7 @@ import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/env/backend_env.dart';
|
||||
import 'package:appflowy/env/env.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
|
||||
@@ -155,6 +156,13 @@ Future<void> _setAppFlowyCloudUrl(String? url) async {
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kAppflowyCloudBaseURL, url ?? '');
|
||||
}
|
||||
|
||||
Future<void> useBaseWebDomain(String? url) async {
|
||||
await getIt<KeyValueStorage>().set(
|
||||
KVKeys.kAppFlowyBaseShareDomain,
|
||||
url ?? ShareConstants.defaultBaseWebDomain,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> useSelfHostedAppFlowyCloudWithURL(String url) async {
|
||||
await _setAuthenticatorType(AuthenticatorType.appflowyCloudSelfHost);
|
||||
await _setAppFlowyCloudUrl(url);
|
||||
@@ -213,6 +221,7 @@ class AppFlowyCloudSharedEnv {
|
||||
ws_base_url: await _getAppFlowyCloudWSUrl(Env.afCloudUrl),
|
||||
gotrue_url: await _getAppFlowyCloudGotrueUrl(Env.afCloudUrl),
|
||||
enable_sync_trace: false,
|
||||
base_web_domain: Env.baseWebDomain,
|
||||
);
|
||||
|
||||
return AppFlowyCloudSharedEnv(
|
||||
@@ -233,6 +242,7 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
|
||||
Uri baseUri,
|
||||
String baseUrl,
|
||||
AuthenticatorType authenticatorType,
|
||||
String baseShareDomain,
|
||||
) async {
|
||||
// In development mode, the app is configured to access the AppFlowy cloud server directly through specific ports.
|
||||
// This setup bypasses the need for Nginx, meaning that the AppFlowy cloud should be running without an Nginx server
|
||||
@@ -243,6 +253,7 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
|
||||
ws_base_url: "ws://${baseUri.host}:8000/ws/v1",
|
||||
gotrue_url: "$baseUrl:9999",
|
||||
enable_sync_trace: true,
|
||||
base_web_domain: ShareConstants.testBaseWebDomain,
|
||||
);
|
||||
} else {
|
||||
return AppFlowyCloudConfiguration(
|
||||
@@ -250,6 +261,9 @@ Future<AppFlowyCloudConfiguration> configurationFromUri(
|
||||
ws_base_url: await _getAppFlowyCloudWSUrl(baseUrl),
|
||||
gotrue_url: await _getAppFlowyCloudGotrueUrl(baseUrl),
|
||||
enable_sync_trace: await getSyncLogEnabled(),
|
||||
base_web_domain: authenticatorType == AuthenticatorType.appflowyCloud
|
||||
? ShareConstants.defaultBaseWebDomain
|
||||
: baseShareDomain,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -258,10 +272,16 @@ Future<AppFlowyCloudConfiguration> getAppFlowyCloudConfig(
|
||||
AuthenticatorType authenticatorType,
|
||||
) async {
|
||||
final baseURL = await getAppFlowyCloudUrl();
|
||||
final baseShareDomain = await getAppFlowyShareDomain();
|
||||
|
||||
try {
|
||||
final uri = Uri.parse(baseURL);
|
||||
return await configurationFromUri(uri, baseURL, authenticatorType);
|
||||
return await configurationFromUri(
|
||||
uri,
|
||||
baseURL,
|
||||
authenticatorType,
|
||||
baseShareDomain,
|
||||
);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse AppFlowy Cloud URL: $e");
|
||||
return AppFlowyCloudConfiguration.defaultConfig();
|
||||
@@ -274,6 +294,12 @@ Future<String> getAppFlowyCloudUrl() async {
|
||||
return result ?? kAppflowyCloudUrl;
|
||||
}
|
||||
|
||||
Future<String> getAppFlowyShareDomain() async {
|
||||
final result =
|
||||
await getIt<KeyValueStorage>().get(KVKeys.kAppFlowyBaseShareDomain);
|
||||
return result ?? ShareConstants.defaultBaseWebDomain;
|
||||
}
|
||||
|
||||
Future<bool> getSyncLogEnabled() async {
|
||||
final result =
|
||||
await getIt<KeyValueStorage>().get(KVKeys.kAppFlowyEnableSyncTrace);
|
||||
|
||||
+8
@@ -1,5 +1,6 @@
|
||||
// lib/env/env.dart
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/shared/share/constants.dart';
|
||||
import 'package:envied/envied.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
@@ -43,4 +44,11 @@ abstract class Env {
|
||||
defaultValue: '',
|
||||
)
|
||||
static const String sentryDsn = _Env.sentryDsn;
|
||||
|
||||
@EnviedField(
|
||||
obfuscate: false,
|
||||
varName: 'BASE_WEB_DOMAIN',
|
||||
defaultValue: ShareConstants.defaultBaseWebDomain,
|
||||
)
|
||||
static const String baseWebDomain = _Env.baseWebDomain;
|
||||
}
|
||||
+7
-6
@@ -6,7 +6,6 @@ import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
@@ -26,11 +25,13 @@ class DocumentPageStyleBloc
|
||||
if (view.id.isEmpty) {
|
||||
return;
|
||||
}
|
||||
final layoutObject =
|
||||
await ViewBackendService.getView(view.id).fold(
|
||||
(s) => jsonDecode(s.extra),
|
||||
(f) => {},
|
||||
);
|
||||
Map layoutObject = {};
|
||||
final data = await ViewBackendService.getView(view.id);
|
||||
data.onSuccess((s) {
|
||||
if (s.extra.isNotEmpty) {
|
||||
layoutObject = jsonDecode(s.extra);
|
||||
}
|
||||
});
|
||||
final fontLayout = _getSelectedFontLayout(layoutObject);
|
||||
final lineHeightLayout = _getSelectedLineHeightLayout(
|
||||
layoutObject,
|
||||
|
||||
@@ -7,6 +7,8 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
import '../../../shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
|
||||
part 'recent_view_bloc.freezed.dart';
|
||||
|
||||
class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
@@ -39,7 +41,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
add(
|
||||
RecentViewEvent.updateNameOrIcon(
|
||||
view.name,
|
||||
view.icon.value,
|
||||
view.icon.toEmojiIconData(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -61,7 +63,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
name: view.name,
|
||||
icon: view.icon.value,
|
||||
icon: view.icon.toEmojiIconData(),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -72,7 +74,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
name: view.name,
|
||||
icon: view.icon.value,
|
||||
icon: view.icon.toEmojiIconData(),
|
||||
coverTypeV2: cover.type,
|
||||
coverValue: cover.value,
|
||||
),
|
||||
@@ -82,7 +84,7 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
emit(
|
||||
state.copyWith(
|
||||
name: view.name,
|
||||
icon: view.icon.value,
|
||||
icon: view.icon.toEmojiIconData(),
|
||||
coverTypeV1: coverTypeV1,
|
||||
coverValue: coverValue,
|
||||
),
|
||||
@@ -135,14 +137,17 @@ class RecentViewBloc extends Bloc<RecentViewEvent, RecentViewState> {
|
||||
@freezed
|
||||
class RecentViewEvent with _$RecentViewEvent {
|
||||
const factory RecentViewEvent.initial() = Initial;
|
||||
|
||||
const factory RecentViewEvent.updateCover(
|
||||
CoverType coverTypeV1, // for the version under 0.5.5, including 0.5.5
|
||||
CoverType coverTypeV1,
|
||||
// for the version under 0.5.5, including 0.5.5
|
||||
PageStyleCoverImageType? coverTypeV2, // for the version above 0.5.5
|
||||
String? coverValue,
|
||||
) = UpdateCover;
|
||||
|
||||
const factory RecentViewEvent.updateNameOrIcon(
|
||||
String name,
|
||||
String icon,
|
||||
EmojiIconData icon,
|
||||
) = UpdateNameOrIcon;
|
||||
}
|
||||
|
||||
@@ -150,12 +155,12 @@ class RecentViewEvent with _$RecentViewEvent {
|
||||
class RecentViewState with _$RecentViewState {
|
||||
const factory RecentViewState({
|
||||
required String name,
|
||||
required String icon,
|
||||
required EmojiIconData icon,
|
||||
@Default(CoverType.none) CoverType coverTypeV1,
|
||||
PageStyleCoverImageType? coverTypeV2,
|
||||
@Default(null) String? coverValue,
|
||||
}) = _RecentViewState;
|
||||
|
||||
factory RecentViewState.initial() =>
|
||||
const RecentViewState(name: '', icon: '');
|
||||
RecentViewState(name: '', icon: EmojiIconData.none());
|
||||
}
|
||||
@@ -7,11 +7,14 @@ import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/feature_flags.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -105,6 +108,12 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
create: (_) =>
|
||||
ShareBloc(view: view)..add(const ShareEvent.initial()),
|
||||
),
|
||||
if (state.userProfilePB != null)
|
||||
BlocProvider(
|
||||
create: (_) =>
|
||||
UserWorkspaceBloc(userProfile: state.userProfilePB!)
|
||||
..add(const UserWorkspaceEvent.initial()),
|
||||
),
|
||||
if (view.layout.isDocumentView)
|
||||
BlocProvider(
|
||||
create: (_) => DocumentPageStyleBloc(view: view)
|
||||
@@ -255,15 +264,14 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
}
|
||||
|
||||
Widget _buildTitle(BuildContext context, ViewPB? view) {
|
||||
final icon = view?.icon.value;
|
||||
final icon = view?.icon;
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (icon != null && icon.isNotEmpty) ...[
|
||||
FlowyText.emoji(
|
||||
icon,
|
||||
fontSize: 15.0,
|
||||
figmaLineHeight: 18.0,
|
||||
if (icon != null && icon.value.isNotEmpty) ...[
|
||||
RawEmojiIconWidget(
|
||||
emoji: icon.toEmojiIconData(),
|
||||
emojiSize: 15,
|
||||
),
|
||||
const HSpace(4),
|
||||
],
|
||||
|
||||
+1
-1
@@ -98,7 +98,7 @@ class BottomSheetBackButton extends StatelessWidget {
|
||||
width: 18,
|
||||
height: 18,
|
||||
child: FlowySvg(
|
||||
FlowySvgs.m_app_bar_back_s,
|
||||
FlowySvgs.m_bottom_sheet_back_s,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
+1
-5
@@ -138,7 +138,6 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
),
|
||||
_divider(),
|
||||
..._buildPublishActions(context),
|
||||
_divider(),
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
textColor: Theme.of(context).colorScheme.error,
|
||||
@@ -205,8 +204,5 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _divider() => const Divider(
|
||||
height: 8.5,
|
||||
thickness: 0.5,
|
||||
);
|
||||
Widget _divider() => const MobileQuickActionDivider();
|
||||
}
|
||||
+29
-13
@@ -57,6 +57,7 @@ Future<T?> showMobileBottomSheet<T>(
|
||||
double maxChildSize = 0.8,
|
||||
double initialChildSize = 0.51,
|
||||
double bottomSheetPadding = 0,
|
||||
bool enablePadding = true,
|
||||
}) async {
|
||||
assert(
|
||||
showHeader ||
|
||||
@@ -172,14 +173,18 @@ Future<T?> showMobileBottomSheet<T>(
|
||||
}
|
||||
|
||||
// ----- content area -----
|
||||
// add content padding and extra bottom padding
|
||||
children.add(
|
||||
Padding(
|
||||
padding:
|
||||
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
if (enablePadding) {
|
||||
// add content padding and extra bottom padding
|
||||
children.add(
|
||||
Padding(
|
||||
padding:
|
||||
padding + EdgeInsets.only(bottom: context.bottomSheetPadding()),
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
children.add(child);
|
||||
}
|
||||
// ----- content area -----
|
||||
|
||||
if (children.length == 1) {
|
||||
@@ -211,14 +216,21 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
required this.showDoneButton,
|
||||
this.onRemove,
|
||||
this.onDone,
|
||||
this.onBack,
|
||||
this.onClose,
|
||||
});
|
||||
|
||||
final String title;
|
||||
|
||||
final bool showBackButton;
|
||||
final bool showCloseButton;
|
||||
final bool showRemoveButton;
|
||||
final String title;
|
||||
final bool showDoneButton;
|
||||
|
||||
final VoidCallback? onRemove;
|
||||
final VoidCallback? onBack;
|
||||
final VoidCallback? onClose;
|
||||
|
||||
final void Function(BuildContext context)? onDone;
|
||||
|
||||
@override
|
||||
@@ -230,14 +242,18 @@ class BottomSheetHeader extends StatelessWidget {
|
||||
child: Stack(
|
||||
children: [
|
||||
if (showBackButton)
|
||||
const Align(
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: BottomSheetBackButton(),
|
||||
child: BottomSheetBackButton(
|
||||
onTap: onBack,
|
||||
),
|
||||
),
|
||||
if (showCloseButton)
|
||||
const Align(
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: BottomSheetCloseButton(),
|
||||
child: BottomSheetCloseButton(
|
||||
onTap: onClose,
|
||||
),
|
||||
),
|
||||
if (showRemoveButton)
|
||||
Align(
|
||||
|
||||
+2
-6
@@ -7,7 +7,6 @@ import 'package:appflowy/util/field_type_extension.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
import 'package:appflowy_board/appflowy_board.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 'package:go_router/go_router.dart';
|
||||
@@ -113,12 +112,8 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
context,
|
||||
showDragHandle: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (_) => SeparatedColumn(
|
||||
builder: (_) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
separatorBuilder: () => const Divider(
|
||||
height: 8.5,
|
||||
thickness: 0.5,
|
||||
),
|
||||
children: [
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.board_column_renameColumn.tr(),
|
||||
@@ -132,6 +127,7 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
const MobileQuickActionDivider(),
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.board_column_hideColumn.tr(),
|
||||
icon: FlowySvgs.hide_s,
|
||||
|
||||
+2
-3
@@ -149,7 +149,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.duplicate_s,
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
const MobileQuickActionDivider(),
|
||||
MobileQuickActionButton(
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
@@ -201,7 +201,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.add_cover_s,
|
||||
text: 'Add cover',
|
||||
),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
const MobileQuickActionDivider(),
|
||||
MobileQuickActionButton(
|
||||
onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
@@ -209,7 +209,6 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.trash_s,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
+2
-5
@@ -48,7 +48,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
context.pop();
|
||||
}
|
||||
}),
|
||||
_divider(),
|
||||
const MobileQuickActionDivider(),
|
||||
_actionButton(
|
||||
context,
|
||||
_Action.duplicate,
|
||||
@@ -58,7 +58,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
},
|
||||
!isInline,
|
||||
),
|
||||
_divider(),
|
||||
const MobileQuickActionDivider(),
|
||||
_actionButton(
|
||||
context,
|
||||
_Action.delete,
|
||||
@@ -68,7 +68,6 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
},
|
||||
!isInline,
|
||||
),
|
||||
_divider(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -88,8 +87,6 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
enable: enable,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider() => const Divider(height: 8.5, thickness: 0.5);
|
||||
}
|
||||
|
||||
enum _Action {
|
||||
|
||||
@@ -2,7 +2,7 @@ import 'package:appflowy/generated/locale_keys.g.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/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
|
||||
import 'package:appflowy/shared/loading.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';
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/util/built_in_svgs.dart';
|
||||
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
|
||||
@@ -228,12 +229,13 @@ class _UserIcon extends StatelessWidget {
|
||||
fontSize: 26,
|
||||
),
|
||||
onTap: () async {
|
||||
final icon = await context.push<EmojiPickerResult>(
|
||||
final icon = await context.push<EmojiIconData>(
|
||||
Uri(
|
||||
path: MobileEmojiPickerScreen.routeName,
|
||||
queryParameters: {
|
||||
MobileEmojiPickerScreen.pageTitle:
|
||||
LocaleKeys.titleBar_userIcon.tr(),
|
||||
MobileEmojiPickerScreen.selectTabs: [PickerTabType.emoji.name],
|
||||
},
|
||||
).toString(),
|
||||
);
|
||||
|
||||
+2
-5
@@ -3,7 +3,7 @@ import 'dart:io';
|
||||
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/plugins/base/emoji/emoji_text.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||
import 'package:appflowy/shared/flowy_gradient_colors.dart';
|
||||
@@ -110,10 +110,7 @@ class MobileRecentView extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: state.icon.isNotEmpty
|
||||
? EmojiText(
|
||||
emoji: state.icon,
|
||||
fontSize: 30.0,
|
||||
)
|
||||
? RawEmojiIconWidget(emoji: state.icon, emojiSize: 30)
|
||||
: SizedBox.square(
|
||||
dimension: 32.0,
|
||||
child: view.defaultIcon(),
|
||||
|
||||
+13
-13
@@ -7,6 +7,7 @@ import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.
|
||||
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
|
||||
import 'package:appflowy/shared/appflowy_network_image.dart';
|
||||
import 'package:appflowy/shared/flowy_gradient_colors.dart';
|
||||
@@ -26,7 +27,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:google_fonts/google_fonts.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
import 'package:string_validator/string_validator.dart';
|
||||
import 'package:time/time.dart';
|
||||
@@ -118,7 +118,7 @@ class MobileViewPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildNameAndLastViewed(BuildContext context, RecentViewState state) {
|
||||
final supportAvatar = isURL(state.icon);
|
||||
final supportAvatar = isURL(state.icon.emoji);
|
||||
if (!supportAvatar) {
|
||||
return _buildLastViewed(context);
|
||||
}
|
||||
@@ -173,23 +173,23 @@ class MobileViewPage extends StatelessWidget {
|
||||
Widget _buildTitle(BuildContext context, RecentViewState state) {
|
||||
final name = state.name;
|
||||
final icon = state.icon;
|
||||
final fontFamily = Platform.isAndroid || Platform.isLinux
|
||||
? GoogleFonts.notoColorEmoji().fontFamily
|
||||
: null;
|
||||
return RichText(
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
text: TextSpan(
|
||||
children: [
|
||||
TextSpan(
|
||||
text: icon,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 17.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontFamily: fontFamily,
|
||||
if (icon.isNotEmpty) ...[
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
child: EmojiIconWidget(
|
||||
emoji: icon,
|
||||
emojiSize: 18.0,
|
||||
),
|
||||
),
|
||||
if (icon.isNotEmpty) const WidgetSpan(child: HSpace(2.0)),
|
||||
),
|
||||
),
|
||||
const WidgetSpan(child: HSpace(8.0)),
|
||||
],
|
||||
TextSpan(
|
||||
text: name,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
|
||||
@@ -14,6 +14,7 @@ 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 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
const _kNotificationIconHeight = 36.0;
|
||||
|
||||
@@ -264,7 +265,11 @@ class NotificationDocumentContent extends StatelessWidget {
|
||||
styleCustomizer: styleCustomizer,
|
||||
// the editor is not editable in the chat
|
||||
editable: false,
|
||||
customHeadingPadding: EdgeInsets.zero,
|
||||
customHeadingPadding: UniversalPlatform.isDesktop
|
||||
? EdgeInsets.zero
|
||||
: EdgeInsets.symmetric(
|
||||
vertical: EditorStyleCustomizer.nodeHorizontalPadding,
|
||||
),
|
||||
);
|
||||
|
||||
return IgnorePointer(
|
||||
|
||||
@@ -2,6 +2,8 @@ import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
@@ -121,6 +123,7 @@ class InnerMobileViewItem extends StatelessWidget {
|
||||
final bool isDraggable;
|
||||
final bool isExpanded;
|
||||
final bool isFirstChild;
|
||||
|
||||
// identify if the view item is rendered as feedback widget inside DraggableItem
|
||||
final bool isFeedback;
|
||||
|
||||
@@ -230,6 +233,7 @@ class SingleMobileInnerViewItem extends StatefulWidget {
|
||||
final ViewPB view;
|
||||
final ViewPB? parentView;
|
||||
final bool isExpanded;
|
||||
|
||||
// identify if the view item is rendered as feedback widget inside DraggableItem
|
||||
final bool isFeedback;
|
||||
|
||||
@@ -297,11 +301,9 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
|
||||
Widget _buildViewIcon() {
|
||||
final icon = widget.view.icon.value.isNotEmpty
|
||||
? FlowyText.emoji(
|
||||
widget.view.icon.value,
|
||||
fontSize: Platform.isAndroid ? 16.0 : 18.0,
|
||||
figmaLineHeight: 20.0,
|
||||
optimizeEmojiAlign: true,
|
||||
? EmojiIconWidget(
|
||||
emoji: widget.view.icon.toEmojiIconData(),
|
||||
emojiSize: Platform.isAndroid ? 16.0 : 18.0,
|
||||
)
|
||||
: Opacity(
|
||||
opacity: 0.7,
|
||||
|
||||
+39
-22
@@ -1,6 +1,7 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
@@ -17,15 +18,15 @@ class RTLSetting extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final layoutDirection =
|
||||
context.watch<AppearanceSettingsCubit>().state.layoutDirection;
|
||||
final textDirection =
|
||||
context.watch<AppearanceSettingsCubit>().state.textDirection;
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_textDirection_label.tr(),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
_textDirectionLabelText(layoutDirection),
|
||||
_textDirectionLabelText(textDirection),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
@@ -39,30 +40,33 @@ class RTLSetting extends StatelessWidget {
|
||||
showDivider: false,
|
||||
title: LocaleKeys.settings_appearance_textDirection_label.tr(),
|
||||
builder: (context) {
|
||||
final layoutDirection =
|
||||
context.watch<AppearanceSettingsCubit>().state.layoutDirection;
|
||||
return Column(
|
||||
children: [
|
||||
FlowyOptionTile.checkbox(
|
||||
text: LocaleKeys.settings_appearance_textDirection_ltr.tr(),
|
||||
isSelected: layoutDirection == LayoutDirection.ltrLayout,
|
||||
onTap: () {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setLayoutDirection(LayoutDirection.ltrLayout);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
isSelected: textDirection == AppFlowyTextDirection.ltr,
|
||||
onTap: () => applyTextDirectionAndPop(
|
||||
context,
|
||||
AppFlowyTextDirection.ltr,
|
||||
),
|
||||
),
|
||||
FlowyOptionTile.checkbox(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.settings_appearance_textDirection_rtl.tr(),
|
||||
isSelected: layoutDirection == LayoutDirection.rtlLayout,
|
||||
onTap: () {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setLayoutDirection(LayoutDirection.rtlLayout);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
isSelected: textDirection == AppFlowyTextDirection.rtl,
|
||||
onTap: () => applyTextDirectionAndPop(
|
||||
context,
|
||||
AppFlowyTextDirection.rtl,
|
||||
),
|
||||
),
|
||||
FlowyOptionTile.checkbox(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.settings_appearance_textDirection_auto.tr(),
|
||||
isSelected: textDirection == AppFlowyTextDirection.auto,
|
||||
onTap: () => applyTextDirectionAndPop(
|
||||
context,
|
||||
AppFlowyTextDirection.auto,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -72,13 +76,26 @@ class RTLSetting extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _textDirectionLabelText(LayoutDirection? textDirection) {
|
||||
String _textDirectionLabelText(AppFlowyTextDirection textDirection) {
|
||||
switch (textDirection) {
|
||||
case LayoutDirection.rtlLayout:
|
||||
case AppFlowyTextDirection.auto:
|
||||
return LocaleKeys.settings_appearance_textDirection_auto.tr();
|
||||
case AppFlowyTextDirection.rtl:
|
||||
return LocaleKeys.settings_appearance_textDirection_rtl.tr();
|
||||
case LayoutDirection.ltrLayout:
|
||||
case AppFlowyTextDirection.ltr:
|
||||
default:
|
||||
return LocaleKeys.settings_appearance_textDirection_ltr.tr();
|
||||
}
|
||||
}
|
||||
|
||||
void applyTextDirectionAndPop(
|
||||
BuildContext context,
|
||||
AppFlowyTextDirection textDirection,
|
||||
) {
|
||||
context.read<AppearanceSettingsCubit>().setTextDirection(textDirection);
|
||||
context
|
||||
.read<DocumentAppearanceCubit>()
|
||||
.syncDefaultTextDirection(textDirection.name);
|
||||
Navigator.pop(context);
|
||||
}
|
||||
}
|
||||
+17
-3
@@ -6,13 +6,20 @@ import 'package:appflowy_backend/log.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
enum SelfHostUrlBottomSheetType {
|
||||
shareDomain,
|
||||
cloudURL,
|
||||
}
|
||||
|
||||
class SelfHostUrlBottomSheet extends StatefulWidget {
|
||||
const SelfHostUrlBottomSheet({
|
||||
super.key,
|
||||
required this.url,
|
||||
required this.type,
|
||||
});
|
||||
|
||||
final String url;
|
||||
final SelfHostUrlBottomSheetType type;
|
||||
|
||||
@override
|
||||
State<SelfHostUrlBottomSheet> createState() => _SelfHostUrlBottomSheetState();
|
||||
@@ -46,8 +53,10 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
|
||||
controller: _textFieldController,
|
||||
keyboardType: TextInputType.text,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return LocaleKeys.settings_mobile_usernameEmptyError.tr();
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
validateUrl(value).isFailure) {
|
||||
return LocaleKeys.settings_menu_invalidCloudURLScheme.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -74,7 +83,12 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
|
||||
if (value.isNotEmpty) {
|
||||
validateUrl(value).fold(
|
||||
(url) async {
|
||||
await useSelfHostedAppFlowyCloudWithURL(url);
|
||||
switch (widget.type) {
|
||||
case SelfHostUrlBottomSheetType.shareDomain:
|
||||
await useBaseWebDomain(url);
|
||||
case SelfHostUrlBottomSheetType.cloudURL:
|
||||
await useSelfHostedAppFlowyCloudWithURL(url);
|
||||
}
|
||||
await runAppFlowy();
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
|
||||
+88
-25
@@ -3,6 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/self_host/self_host_bottom_sheet.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'setting.dart';
|
||||
@@ -17,44 +18,106 @@ class SelfHostSettingGroup extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SelfHostSettingGroupState extends State<SelfHostSettingGroup> {
|
||||
final future = getAppFlowyCloudUrl();
|
||||
final future = Future.wait([
|
||||
getAppFlowyCloudUrl(),
|
||||
getAppFlowyShareDomain(),
|
||||
]);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, snapshot) {
|
||||
if (!snapshot.hasData) {
|
||||
final data = snapshot.data;
|
||||
if (!snapshot.hasData || data == null || data.length != 2) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final url = snapshot.data ?? '';
|
||||
final url = data[0];
|
||||
final shareDomain = data[1];
|
||||
return MobileSettingGroup(
|
||||
groupTitle: LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr(),
|
||||
groupTitle: LocaleKeys.settings_menu_cloudAppFlowy.tr(),
|
||||
settingItemList: [
|
||||
MobileSettingItem(
|
||||
name: url,
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: LocaleKeys.editor_urlHint.tr(),
|
||||
showCloseButton: true,
|
||||
showDivider: false,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
builder: (_) {
|
||||
return SelfHostUrlBottomSheet(
|
||||
url: url,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
_buildSelfHostField(url),
|
||||
_buildShareDomainField(shareDomain),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSelfHostField(String url) {
|
||||
return MobileSettingItem(
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.settings_menu_cloudURL.tr(),
|
||||
fontSize: 12.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
subtitle: FlowyText(
|
||||
url,
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: LocaleKeys.editor_urlHint.tr(),
|
||||
showCloseButton: true,
|
||||
showDivider: false,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
builder: (_) {
|
||||
return SelfHostUrlBottomSheet(
|
||||
url: url,
|
||||
type: SelfHostUrlBottomSheetType.cloudURL,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildShareDomainField(String shareDomain) {
|
||||
return MobileSettingItem(
|
||||
title: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 4.0),
|
||||
child: FlowyText(
|
||||
LocaleKeys.settings_menu_webURL.tr(),
|
||||
fontSize: 12.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
),
|
||||
),
|
||||
subtitle: FlowyText(
|
||||
shareDomain,
|
||||
),
|
||||
trailing: const Icon(
|
||||
Icons.chevron_right,
|
||||
),
|
||||
onTap: () {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showHeader: true,
|
||||
title: LocaleKeys.editor_urlHint.tr(),
|
||||
showCloseButton: true,
|
||||
showDivider: false,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16.0,
|
||||
vertical: 8.0,
|
||||
),
|
||||
builder: (_) {
|
||||
return SelfHostUrlBottomSheet(
|
||||
url: shareDomain,
|
||||
type: SelfHostUrlBottomSheetType.shareDomain,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+23
-17
@@ -4,41 +4,29 @@ import 'package:flutter/material.dart';
|
||||
class MobileSettingItem extends StatelessWidget {
|
||||
const MobileSettingItem({
|
||||
super.key,
|
||||
required this.name,
|
||||
this.name,
|
||||
this.padding = const EdgeInsets.only(bottom: 4),
|
||||
this.trailing,
|
||||
this.leadingIcon,
|
||||
this.title,
|
||||
this.subtitle,
|
||||
this.onTap,
|
||||
});
|
||||
|
||||
final String name;
|
||||
final String? name;
|
||||
final EdgeInsets padding;
|
||||
final Widget? trailing;
|
||||
final Widget? leadingIcon;
|
||||
final Widget? subtitle;
|
||||
final VoidCallback? onTap;
|
||||
final Widget? title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: padding,
|
||||
child: ListTile(
|
||||
title: Row(
|
||||
children: [
|
||||
if (leadingIcon != null) ...[
|
||||
leadingIcon!,
|
||||
const HSpace(8),
|
||||
],
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
name,
|
||||
fontSize: 14.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
title: title ?? _buildDefaultTitle(name),
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
onTap: onTap,
|
||||
@@ -47,4 +35,22 @@ class MobileSettingItem extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDefaultTitle(String? name) {
|
||||
return Row(
|
||||
children: [
|
||||
if (leadingIcon != null) ...[
|
||||
leadingIcon!,
|
||||
const HSpace(8),
|
||||
],
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
name ?? '',
|
||||
fontSize: 14.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+32
-27
@@ -25,36 +25,41 @@ class MobileQuickActionButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconSize = this.iconSize ?? const Size.square(18);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: InkWell(
|
||||
onTap: enable ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
overlayColor:
|
||||
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
|
||||
splashColor: Colors.transparent,
|
||||
child: Container(
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: enable ? iconColor : Theme.of(context).disabledColor,
|
||||
return InkWell(
|
||||
onTap: enable ? onTap : null,
|
||||
overlayColor:
|
||||
enable ? null : const WidgetStatePropertyAll(Colors.transparent),
|
||||
splashColor: Colors.transparent,
|
||||
child: Container(
|
||||
height: 52,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Row(
|
||||
children: [
|
||||
FlowySvg(
|
||||
icon,
|
||||
size: iconSize,
|
||||
color: enable ? iconColor : Theme.of(context).disabledColor,
|
||||
),
|
||||
HSpace(30 - iconSize.width),
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
text,
|
||||
fontSize: 16,
|
||||
color: enable ? textColor : Theme.of(context).disabledColor,
|
||||
),
|
||||
HSpace(30 - iconSize.width),
|
||||
Expanded(
|
||||
child: FlowyText.regular(
|
||||
text,
|
||||
fontSize: 16,
|
||||
color: enable ? textColor : Theme.of(context).disabledColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileQuickActionDivider extends StatelessWidget {
|
||||
const MobileQuickActionDivider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Divider(height: 0.5, thickness: 0.5);
|
||||
}
|
||||
}
|
||||
@@ -37,8 +37,13 @@ class AIPromptInputBloc extends Bloc<AIPromptInputEvent, AIPromptInputState> {
|
||||
// Only user enable chat with file and the plugin is already running
|
||||
final supportChatWithFile = chatState.fileEnabled &&
|
||||
chatState.pluginState.state == RunningStatePB.Running;
|
||||
|
||||
final aiType = chatState.pluginState.state == RunningStatePB.Running
|
||||
? AIType.localAI
|
||||
: AIType.appflowyAI;
|
||||
emit(
|
||||
state.copyWith(
|
||||
aiType: aiType,
|
||||
supportChatWithFile: supportChatWithFile,
|
||||
chatState: chatState,
|
||||
),
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:appflowy_backend/dispatch/dispatch.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/code.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:fixnum/fixnum.dart';
|
||||
@@ -30,7 +31,8 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
super(ChatState.initial()) {
|
||||
_startListening();
|
||||
_dispatch();
|
||||
_init();
|
||||
_loadMessages();
|
||||
_loadSetting();
|
||||
}
|
||||
|
||||
final String chatId;
|
||||
@@ -57,22 +59,16 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
bool isLoadingPreviousMessages = false;
|
||||
bool hasMorePreviousMessages = true;
|
||||
AnswerStream? answerStream;
|
||||
int numSendMessage = 0;
|
||||
|
||||
/// a counter used to determine whether the initial loading state should be
|
||||
/// set to finish. It should hit two before we emit: one for the local fetch
|
||||
/// and another for the server fetch.
|
||||
///
|
||||
/// This is to work around a bug where if an ai chat that is not yet on the
|
||||
/// user local storage is opened but has messages in the server, it will
|
||||
/// remain stuck on the welcome screen until the user switches to another page
|
||||
/// then come back.
|
||||
int initialFetchCounter = 0;
|
||||
bool isFetchingRelatedQuestions = false;
|
||||
bool shouldFetchRelatedQuestions = false;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
await answerStream?.dispose();
|
||||
await listener.stop();
|
||||
final request = ViewIdPB(value: chatId);
|
||||
unawaited(FolderEventCloseView(request).send());
|
||||
|
||||
return super.close();
|
||||
}
|
||||
|
||||
@@ -86,14 +82,23 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
await chatController.insert(message, index: 0);
|
||||
}
|
||||
|
||||
if (initialFetchCounter < 2) {
|
||||
initialFetchCounter++;
|
||||
}
|
||||
|
||||
if (state.loadingState.isLoading && initialFetchCounter >= 2) {
|
||||
emit(
|
||||
state.copyWith(loadingState: const ChatLoadingState.finish()),
|
||||
);
|
||||
switch (state.loadingState) {
|
||||
case LoadChatMessageStatus.loading
|
||||
when chatController.messages.isEmpty:
|
||||
emit(
|
||||
state.copyWith(
|
||||
loadingState: LoadChatMessageStatus.loadingRemote,
|
||||
),
|
||||
);
|
||||
break;
|
||||
case LoadChatMessageStatus.loading:
|
||||
case LoadChatMessageStatus.loadingRemote:
|
||||
emit(
|
||||
state.copyWith(loadingState: LoadChatMessageStatus.ready),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
loadPreviousMessages: () {
|
||||
@@ -161,36 +166,28 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
},
|
||||
sendMessage: (
|
||||
String message,
|
||||
PredefinedFormat? format,
|
||||
Map<String, dynamic>? metadata,
|
||||
) {
|
||||
numSendMessage += 1;
|
||||
|
||||
final relatedQuestionMessages = chatController.messages
|
||||
.where(
|
||||
(message) =>
|
||||
onetimeMessageTypeFromMeta(message.metadata) ==
|
||||
OnetimeShotType.relatedQuestion,
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final message in relatedQuestionMessages) {
|
||||
chatController.remove(message);
|
||||
}
|
||||
|
||||
_startStreamingMessage(message, metadata);
|
||||
_clearErrorMessages(emit);
|
||||
_clearRelatedQuestions();
|
||||
_startStreamingMessage(message, format, metadata);
|
||||
lastSentMessage = null;
|
||||
|
||||
isFetchingRelatedQuestions = false;
|
||||
shouldFetchRelatedQuestions =
|
||||
format == null || format.imageFormat.hasText;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
promptResponseState: PromptResponseState.sendingQuestion,
|
||||
),
|
||||
);
|
||||
},
|
||||
finishSending: (ChatMessagePB message) {
|
||||
lastSentMessage = message;
|
||||
finishSending: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
promptResponseState: PromptResponseState.awaitingAnswer,
|
||||
promptResponseState: PromptResponseState.streamingAnswer,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -228,14 +225,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
answerStream = null;
|
||||
answerStreamMessageId = '';
|
||||
},
|
||||
startAnswerStreaming: (Message message) {
|
||||
chatController.insert(message);
|
||||
emit(
|
||||
state.copyWith(
|
||||
promptResponseState: PromptResponseState.streamingAnswer,
|
||||
),
|
||||
);
|
||||
},
|
||||
failedSending: () {
|
||||
final lastMessage = chatController.messages.lastOrNull;
|
||||
if (lastMessage != null) {
|
||||
@@ -247,6 +236,39 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
regenerateAnswer: (id, format) {
|
||||
_clearRelatedQuestions();
|
||||
_regenerateAnswer(id, format);
|
||||
lastSentMessage = null;
|
||||
|
||||
isFetchingRelatedQuestions = false;
|
||||
shouldFetchRelatedQuestions = false;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
promptResponseState: PromptResponseState.sendingQuestion,
|
||||
),
|
||||
);
|
||||
},
|
||||
didReceiveChatSettings: (settings) {
|
||||
emit(
|
||||
state.copyWith(selectedSourceIds: settings.ragIds),
|
||||
);
|
||||
},
|
||||
updateSelectedSources: (selectedSourcesIds) async {
|
||||
emit(state.copyWith(selectedSourceIds: selectedSourcesIds));
|
||||
|
||||
final payload = UpdateChatSettingsPB(
|
||||
chatId: ChatId(value: chatId),
|
||||
ragIds: selectedSourcesIds,
|
||||
);
|
||||
await AIEventUpdateChatSettings(payload)
|
||||
.send()
|
||||
.onFailure(Log.error);
|
||||
},
|
||||
deleteMessage: (mesesage) async {
|
||||
await chatController.remove(mesesage);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -261,15 +283,19 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
|
||||
// 3 mean message response from AI
|
||||
if (pb.authorType == 3 && answerStreamMessageId.isNotEmpty) {
|
||||
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||
answerStreamMessageId;
|
||||
temporaryMessageIDMap.putIfAbsent(
|
||||
pb.messageId.toString(),
|
||||
() => answerStreamMessageId,
|
||||
);
|
||||
answerStreamMessageId = '';
|
||||
}
|
||||
|
||||
// 1 mean message response from User
|
||||
if (pb.authorType == 1 && questionStreamMessageId.isNotEmpty) {
|
||||
temporaryMessageIDMap[pb.messageId.toString()] =
|
||||
questionStreamMessageId;
|
||||
temporaryMessageIDMap.putIfAbsent(
|
||||
pb.messageId.toString(),
|
||||
() => questionStreamMessageId,
|
||||
);
|
||||
questionStreamMessageId = '';
|
||||
}
|
||||
|
||||
@@ -304,7 +330,9 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
// The answer stream will bet set to null after the streaming has
|
||||
// finished, got cancelled, or errored. In this case, don't retrieve
|
||||
// related questions.
|
||||
if (answerStream == null || lastSentMessage == null) {
|
||||
if (answerStream == null ||
|
||||
lastSentMessage == null ||
|
||||
!shouldFetchRelatedQuestions) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -313,17 +341,19 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
messageId: lastSentMessage!.messageId,
|
||||
);
|
||||
|
||||
// when previous numSendMessage is not equal to current numSendMessage, it means that the user
|
||||
// has sent a new message. So we don't need to get related questions.
|
||||
final preNumSendMessage = numSendMessage;
|
||||
isFetchingRelatedQuestions = true;
|
||||
await AIEventGetRelatedQuestion(payload).send().fold(
|
||||
(list) {
|
||||
if (!isClosed && preNumSendMessage == numSendMessage) {
|
||||
// while fetching related questions, the user might enter a new
|
||||
// question or regenerate a previous response. In such cases, don't
|
||||
// display the relatedQuestions
|
||||
if (!isClosed && isFetchingRelatedQuestions) {
|
||||
add(
|
||||
ChatEvent.didReceiveRelatedQuestions(
|
||||
list.items.map((e) => e.content).toList(),
|
||||
),
|
||||
);
|
||||
isFetchingRelatedQuestions = false;
|
||||
}
|
||||
},
|
||||
(err) => Log.error("Failed to get related questions: $err"),
|
||||
@@ -332,12 +362,25 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _init() async {
|
||||
final payload = LoadNextChatMessagePB(
|
||||
void _loadSetting() async {
|
||||
final getChatSettingsPayload =
|
||||
AIEventGetChatSettings(ChatId(value: chatId));
|
||||
await getChatSettingsPayload.send().fold(
|
||||
(settings) {
|
||||
if (!isClosed) {
|
||||
add(ChatEvent.didReceiveChatSettings(settings: settings));
|
||||
}
|
||||
},
|
||||
Log.error,
|
||||
);
|
||||
}
|
||||
|
||||
void _loadMessages() async {
|
||||
final loadMessagesPayload = LoadNextChatMessagePB(
|
||||
chatId: chatId,
|
||||
limit: Int64(10),
|
||||
);
|
||||
await AIEventLoadNextMessage(payload).send().fold(
|
||||
await AIEventLoadNextMessage(loadMessagesPayload).send().fold(
|
||||
(list) {
|
||||
if (!isClosed) {
|
||||
final messages = list.messages.map(_createTextMessage).toList();
|
||||
@@ -370,6 +413,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
|
||||
Future<void> _startStreamingMessage(
|
||||
String message,
|
||||
PredefinedFormat? format,
|
||||
Map<String, dynamic>? metadata,
|
||||
) async {
|
||||
await answerStream?.dispose();
|
||||
@@ -392,18 +436,23 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
answerStreamPort: Int64(answerStream!.nativePort),
|
||||
metadata: await metadataPBFromMetadata(metadata),
|
||||
);
|
||||
if (format != null) {
|
||||
payload.format = format.toPB();
|
||||
}
|
||||
|
||||
// stream the question to the server
|
||||
await AIEventStreamMessage(payload).send().fold(
|
||||
(question) {
|
||||
if (!isClosed) {
|
||||
final streamAnswer = _createAnswerStreamMessage(
|
||||
answerStream!,
|
||||
question.messageId,
|
||||
stream: answerStream!,
|
||||
questionMessageId: question.messageId,
|
||||
fakeQuestionMessageId: questionStreamMessage.id,
|
||||
);
|
||||
|
||||
add(ChatEvent.finishSending(question));
|
||||
add(ChatEvent.startAnswerStreaming(streamAnswer));
|
||||
lastSentMessage = question;
|
||||
add(const ChatEvent.finishSending());
|
||||
add(ChatEvent.receiveMessage(streamAnswer));
|
||||
}
|
||||
},
|
||||
(err) {
|
||||
@@ -430,11 +479,56 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
}
|
||||
|
||||
Message _createAnswerStreamMessage(
|
||||
AnswerStream stream,
|
||||
Int64 questionMessageId,
|
||||
) {
|
||||
answerStreamMessageId = (questionMessageId + 1).toString();
|
||||
void _regenerateAnswer(
|
||||
String answerMessageIdString,
|
||||
PredefinedFormat? format,
|
||||
) async {
|
||||
final id = temporaryMessageIDMap.entries
|
||||
.firstWhereOrNull((e) => e.value == answerMessageIdString)
|
||||
?.key ??
|
||||
answerMessageIdString;
|
||||
final answerMessageId = Int64.tryParseInt(id);
|
||||
|
||||
if (answerMessageId == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
await answerStream?.dispose();
|
||||
answerStream = AnswerStream();
|
||||
|
||||
final payload = RegenerateResponsePB(
|
||||
chatId: chatId,
|
||||
answerMessageId: answerMessageId,
|
||||
answerStreamPort: Int64(answerStream!.nativePort),
|
||||
);
|
||||
if (format != null) {
|
||||
payload.format = format.toPB();
|
||||
}
|
||||
|
||||
await AIEventRegenerateResponse(payload).send().fold(
|
||||
(success) {
|
||||
if (!isClosed) {
|
||||
final streamAnswer = _createAnswerStreamMessage(
|
||||
stream: answerStream!,
|
||||
questionMessageId: answerMessageId - 1,
|
||||
).copyWith(id: answerMessageIdString);
|
||||
|
||||
add(ChatEvent.receiveMessage(streamAnswer));
|
||||
add(const ChatEvent.finishSending());
|
||||
}
|
||||
},
|
||||
(err) => Log.error("Failed to send message: ${err.msg}"),
|
||||
);
|
||||
}
|
||||
|
||||
Message _createAnswerStreamMessage({
|
||||
required AnswerStream stream,
|
||||
required Int64 questionMessageId,
|
||||
String? fakeQuestionMessageId,
|
||||
}) {
|
||||
answerStreamMessageId = fakeQuestionMessageId == null
|
||||
? (questionMessageId + 1).toString()
|
||||
: "${fakeQuestionMessageId}_ans";
|
||||
|
||||
return TextMessage(
|
||||
id: answerStreamMessageId,
|
||||
@@ -488,22 +582,63 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _clearErrorMessages(Emitter<ChatState> emit) {
|
||||
final errorMessages = chatController.messages
|
||||
.where(
|
||||
(message) =>
|
||||
onetimeMessageTypeFromMeta(message.metadata) ==
|
||||
OnetimeShotType.error,
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final message in errorMessages) {
|
||||
chatController.remove(message);
|
||||
}
|
||||
emit(state.copyWith(clearErrorMessages: !state.clearErrorMessages));
|
||||
}
|
||||
|
||||
void _clearRelatedQuestions() {
|
||||
final relatedQuestionMessages = chatController.messages
|
||||
.where(
|
||||
(message) =>
|
||||
onetimeMessageTypeFromMeta(message.metadata) ==
|
||||
OnetimeShotType.relatedQuestion,
|
||||
)
|
||||
.toList();
|
||||
|
||||
for (final message in relatedQuestionMessages) {
|
||||
chatController.remove(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatEvent with _$ChatEvent {
|
||||
// chat settings
|
||||
const factory ChatEvent.didReceiveChatSettings({
|
||||
required ChatSettingsPB settings,
|
||||
}) = _DidReceiveChatSettings;
|
||||
const factory ChatEvent.updateSelectedSources({
|
||||
required List<String> selectedSourcesIds,
|
||||
}) = _UpdateSelectedSources;
|
||||
|
||||
// send message
|
||||
const factory ChatEvent.sendMessage({
|
||||
required String message,
|
||||
PredefinedFormat? format,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) = _SendMessage;
|
||||
const factory ChatEvent.finishSending(ChatMessagePB message) =
|
||||
_FinishSendMessage;
|
||||
const factory ChatEvent.finishSending() = _FinishSendMessage;
|
||||
const factory ChatEvent.failedSending() = _FailSendMessage;
|
||||
|
||||
// regenerate
|
||||
const factory ChatEvent.regenerateAnswer(
|
||||
String id,
|
||||
PredefinedFormat? format,
|
||||
) = _RegenerateAnswer;
|
||||
|
||||
// streaming answer
|
||||
const factory ChatEvent.startAnswerStreaming(Message message) =
|
||||
_StartAnswerStreaming;
|
||||
const factory ChatEvent.stopStream() = _StopStream;
|
||||
const factory ChatEvent.didFinishAnswerStream() = _DidFinishAnswerStream;
|
||||
|
||||
@@ -523,18 +658,24 @@ class ChatEvent with _$ChatEvent {
|
||||
const factory ChatEvent.didReceiveRelatedQuestions(
|
||||
List<String> questions,
|
||||
) = _DidReceiveRelatedQueston;
|
||||
|
||||
const factory ChatEvent.deleteMessage(Message message) = _DeleteMessage;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatState with _$ChatState {
|
||||
const factory ChatState({
|
||||
required ChatLoadingState loadingState,
|
||||
required List<String> selectedSourceIds,
|
||||
required LoadChatMessageStatus loadingState,
|
||||
required PromptResponseState promptResponseState,
|
||||
required bool clearErrorMessages,
|
||||
}) = _ChatState;
|
||||
|
||||
factory ChatState.initial() => const ChatState(
|
||||
loadingState: ChatLoadingState.loading(),
|
||||
selectedSourceIds: [],
|
||||
loadingState: LoadChatMessageStatus.loading,
|
||||
promptResponseState: PromptResponseState.ready,
|
||||
clearErrorMessages: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
|
||||
import 'package:appflowy/plugins/document/application/prelude.dart';
|
||||
import 'package:appflowy/shared/markdown_to_document.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||
|
||||
class ChatEditDocumentService {
|
||||
static Future<ViewPB?> saveMessagesToNewPage(
|
||||
String chatPageName,
|
||||
String parentViewId,
|
||||
List<TextMessage> messages,
|
||||
) async {
|
||||
if (messages.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Convert messages to markdown and trim the last empty newline.
|
||||
final completeMessage = messages.map((m) => m.text).join('\n').trimRight();
|
||||
if (completeMessage.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final document = customMarkdownToDocument(completeMessage);
|
||||
final initialBytes =
|
||||
DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
|
||||
if (initialBytes == null) {
|
||||
Log.error('Failed to convert messages to document');
|
||||
return null;
|
||||
}
|
||||
|
||||
return ViewBackendService.createView(
|
||||
name: LocaleKeys.chat_addToNewPageName.tr(args: [chatPageName]),
|
||||
layoutType: ViewLayoutPB.Document,
|
||||
parentViewId: parentViewId,
|
||||
initialDataBytes: initialBytes,
|
||||
).toNullable();
|
||||
}
|
||||
|
||||
static Future<void> addMessageToPage(
|
||||
String documentId,
|
||||
TextMessage message,
|
||||
) async {
|
||||
if (message.text.isEmpty) {
|
||||
Log.error('Message is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
final bloc = DocumentBloc(
|
||||
documentId: documentId,
|
||||
saveToBlocMap: false,
|
||||
)..add(const DocumentEvent.initial());
|
||||
|
||||
if (bloc.state.editorState == null) {
|
||||
await bloc.stream.firstWhere((state) => state.editorState != null);
|
||||
}
|
||||
|
||||
final editorState = bloc.state.editorState;
|
||||
if (editorState == null) {
|
||||
Log.error("Can't get EditorState of document");
|
||||
return;
|
||||
}
|
||||
|
||||
final messageDocument = customMarkdownToDocument(message.text);
|
||||
if (messageDocument.isEmpty) {
|
||||
Log.error('Failed to convert message to document');
|
||||
return;
|
||||
}
|
||||
|
||||
final lastNodeOrNull = editorState.document.root.children.lastOrNull;
|
||||
|
||||
final rootIsEmpty = lastNodeOrNull == null;
|
||||
final isLastLineEmpty = lastNodeOrNull?.children.isNotEmpty == false &&
|
||||
lastNodeOrNull?.delta?.isNotEmpty == false;
|
||||
|
||||
final nodes = [
|
||||
if (rootIsEmpty || !isLastLineEmpty) paragraphNode(),
|
||||
...messageDocument.root.children,
|
||||
paragraphNode(),
|
||||
];
|
||||
final insertPath = rootIsEmpty ||
|
||||
listEquals(lastNodeOrNull.path, const [0]) && isLastLineEmpty
|
||||
? const [0]
|
||||
: lastNodeOrNull.path.next;
|
||||
|
||||
final transaction = editorState.transaction..insertNodes(insertPath, nodes);
|
||||
await editorState.apply(transaction);
|
||||
await bloc.close();
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/entities.pbenum.dart';
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-ai/protobuf.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-error/errors.pb.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
@@ -57,7 +60,6 @@ class AIChatProgress {
|
||||
enum PromptResponseState {
|
||||
ready,
|
||||
sendingQuestion,
|
||||
awaitingAnswer,
|
||||
streamingAnswer,
|
||||
}
|
||||
|
||||
@@ -132,3 +134,93 @@ const onetimeShotType = "OnetimeShotType";
|
||||
OnetimeShotType? onetimeMessageTypeFromMeta(Map<String, dynamic>? metadata) {
|
||||
return metadata?[onetimeShotType];
|
||||
}
|
||||
|
||||
enum LoadChatMessageStatus {
|
||||
loading,
|
||||
loadingRemote,
|
||||
ready,
|
||||
}
|
||||
|
||||
class PredefinedFormat extends Equatable {
|
||||
const PredefinedFormat({
|
||||
required this.imageFormat,
|
||||
required this.textFormat,
|
||||
});
|
||||
|
||||
const PredefinedFormat.auto()
|
||||
: imageFormat = ImageFormat.text,
|
||||
textFormat = TextFormat.auto;
|
||||
|
||||
final ImageFormat imageFormat;
|
||||
final TextFormat? textFormat;
|
||||
|
||||
PredefinedFormatPB toPB() {
|
||||
return PredefinedFormatPB(
|
||||
imageFormat: switch (imageFormat) {
|
||||
ImageFormat.text => ResponseImageFormatPB.TextOnly,
|
||||
ImageFormat.image => ResponseImageFormatPB.ImageOnly,
|
||||
ImageFormat.textAndImage => ResponseImageFormatPB.TextAndImage,
|
||||
},
|
||||
textFormat: switch (textFormat) {
|
||||
TextFormat.auto => ResponseTextFormatPB.Paragraph,
|
||||
TextFormat.bulletList => ResponseTextFormatPB.BulletedList,
|
||||
TextFormat.numberedList => ResponseTextFormatPB.NumberedList,
|
||||
TextFormat.table => ResponseTextFormatPB.Table,
|
||||
_ => null,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [imageFormat, textFormat];
|
||||
}
|
||||
|
||||
enum ImageFormat {
|
||||
text,
|
||||
image,
|
||||
textAndImage;
|
||||
|
||||
bool get hasText => this == text || this == textAndImage;
|
||||
|
||||
FlowySvgData get icon {
|
||||
return switch (this) {
|
||||
ImageFormat.text => FlowySvgs.ai_text_s,
|
||||
ImageFormat.image => FlowySvgs.ai_image_s,
|
||||
ImageFormat.textAndImage => FlowySvgs.ai_text_image_s,
|
||||
};
|
||||
}
|
||||
|
||||
String get i18n {
|
||||
return switch (this) {
|
||||
ImageFormat.text => LocaleKeys.chat_changeFormat_textOnly.tr(),
|
||||
ImageFormat.image => LocaleKeys.chat_changeFormat_imageOnly.tr(),
|
||||
ImageFormat.textAndImage =>
|
||||
LocaleKeys.chat_changeFormat_textAndImage.tr(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
enum TextFormat {
|
||||
auto,
|
||||
bulletList,
|
||||
numberedList,
|
||||
table;
|
||||
|
||||
FlowySvgData get icon {
|
||||
return switch (this) {
|
||||
TextFormat.auto => FlowySvgs.ai_paragraph_s,
|
||||
TextFormat.bulletList => FlowySvgs.ai_list_s,
|
||||
TextFormat.numberedList => FlowySvgs.ai_number_list_s,
|
||||
TextFormat.table => FlowySvgs.ai_table_s,
|
||||
};
|
||||
}
|
||||
|
||||
String get i18n {
|
||||
return switch (this) {
|
||||
TextFormat.auto => LocaleKeys.chat_changeFormat_text.tr(),
|
||||
TextFormat.bulletList => LocaleKeys.chat_changeFormat_bullet.tr(),
|
||||
TextFormat.numberedList => LocaleKeys.chat_changeFormat_number.tr(),
|
||||
TextFormat.table => LocaleKeys.chat_changeFormat_table.tr(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -31,6 +31,7 @@ class AnswerStream {
|
||||
_onEnd?.call();
|
||||
},
|
||||
onError: (error) {
|
||||
_error = error.toString();
|
||||
_onError?.call(error.toString());
|
||||
},
|
||||
);
|
||||
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:appflowy_result/appflowy_result.dart';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'chat_select_sources_cubit.freezed.dart';
|
||||
|
||||
const int _kMaxSelectedParentPageCount = 3;
|
||||
|
||||
enum SourceSelectedStatus {
|
||||
unselected,
|
||||
selected,
|
||||
partiallySelected;
|
||||
|
||||
bool get isUnselected => this == unselected;
|
||||
bool get isSelected => this == selected;
|
||||
bool get isPartiallySelected => this == partiallySelected;
|
||||
}
|
||||
|
||||
class ChatSource {
|
||||
ChatSource({
|
||||
required this.view,
|
||||
required this.parentView,
|
||||
required this.children,
|
||||
required bool isExpanded,
|
||||
required SourceSelectedStatus selectedStatus,
|
||||
required IgnoreViewType ignoreStatus,
|
||||
}) : isExpandedNotifier = ValueNotifier(isExpanded),
|
||||
selectedStatusNotifier = ValueNotifier(selectedStatus),
|
||||
ignoreStatusNotifier = ValueNotifier(ignoreStatus);
|
||||
|
||||
final ViewPB view;
|
||||
final ViewPB? parentView;
|
||||
final List<ChatSource> children;
|
||||
final ValueNotifier<bool> isExpandedNotifier;
|
||||
final ValueNotifier<SourceSelectedStatus> selectedStatusNotifier;
|
||||
final ValueNotifier<IgnoreViewType> ignoreStatusNotifier;
|
||||
|
||||
bool get isExpanded => isExpandedNotifier.value;
|
||||
SourceSelectedStatus get selectedStatus => selectedStatusNotifier.value;
|
||||
IgnoreViewType get ignoreStatus => ignoreStatusNotifier.value;
|
||||
|
||||
void toggleIsExpanded() {
|
||||
isExpandedNotifier.value = !isExpanded;
|
||||
}
|
||||
|
||||
ChatSource copy() {
|
||||
return ChatSource(
|
||||
view: view,
|
||||
parentView: parentView,
|
||||
children: children.map<ChatSource>((child) => child.copy()).toList(),
|
||||
ignoreStatus: ignoreStatus,
|
||||
isExpanded: isExpanded,
|
||||
selectedStatus: selectedStatus,
|
||||
);
|
||||
}
|
||||
|
||||
ChatSource? findChildBySourceId(String sourceId) {
|
||||
if (view.id == sourceId) {
|
||||
return this;
|
||||
}
|
||||
for (final child in children) {
|
||||
final childResult = child.findChildBySourceId(sourceId);
|
||||
if (childResult != null) {
|
||||
return childResult;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void resetIgnoreViewTypeRecursive() {
|
||||
ignoreStatusNotifier.value = view.layout.isDocumentView
|
||||
? IgnoreViewType.none
|
||||
: IgnoreViewType.disable;
|
||||
|
||||
for (final child in children) {
|
||||
child.resetIgnoreViewTypeRecursive();
|
||||
}
|
||||
}
|
||||
|
||||
void updateIgnoreViewTypeRecursive(IgnoreViewType newIgnoreViewType) {
|
||||
ignoreStatusNotifier.value = newIgnoreViewType;
|
||||
for (final child in children) {
|
||||
child.updateIgnoreViewTypeRecursive(newIgnoreViewType);
|
||||
}
|
||||
}
|
||||
|
||||
void dispose() {
|
||||
for (final child in children) {
|
||||
child.dispose();
|
||||
}
|
||||
isExpandedNotifier.dispose();
|
||||
selectedStatusNotifier.dispose();
|
||||
ignoreStatusNotifier.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
ChatSettingsCubit({
|
||||
this.hideDisabled = false,
|
||||
}) : super(ChatSettingsState.initial());
|
||||
|
||||
final bool hideDisabled;
|
||||
|
||||
final List<String> selectedSourceIds = [];
|
||||
final List<ChatSource> sources = [];
|
||||
final List<ChatSource> selectedSources = [];
|
||||
String filter = '';
|
||||
|
||||
void updateSelectedSources(List<String> newSelectedSourceIds) {
|
||||
selectedSourceIds
|
||||
..clear
|
||||
..addAll(newSelectedSourceIds);
|
||||
}
|
||||
|
||||
void refreshSources(List<ViewPB> spaceViews, ViewPB? currentSpace) async {
|
||||
filter = "";
|
||||
|
||||
final newSources = await Future.wait(
|
||||
spaceViews.map((view) => _recursiveBuild(view, null)),
|
||||
);
|
||||
for (final source in newSources) {
|
||||
_restrictSelectionIfNecessary(source.children);
|
||||
}
|
||||
if (currentSpace != null) {
|
||||
newSources
|
||||
.firstWhereOrNull((e) => e.view.id == currentSpace.id)
|
||||
?.toggleIsExpanded();
|
||||
}
|
||||
|
||||
final selected = newSources
|
||||
.map((source) => _buildSelectedSources(source))
|
||||
.flattened
|
||||
.toList();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedSources: selected,
|
||||
visibleSources: newSources,
|
||||
),
|
||||
);
|
||||
|
||||
sources
|
||||
..forEach((e) => e.dispose())
|
||||
..clear()
|
||||
..addAll(newSources.map((e) => e.copy()));
|
||||
|
||||
selectedSources
|
||||
..forEach((e) => e.dispose())
|
||||
..clear()
|
||||
..addAll(selected.map((e) => e.copy()));
|
||||
}
|
||||
|
||||
Future<ChatSource> _recursiveBuild(ViewPB view, ViewPB? parentView) async {
|
||||
SourceSelectedStatus selectedStatus = SourceSelectedStatus.unselected;
|
||||
final isThisSourceSelected = selectedSourceIds.contains(view.id);
|
||||
|
||||
final childrenViews =
|
||||
await ViewBackendService.getChildViews(viewId: view.id).toNullable();
|
||||
|
||||
int selectedCount = 0;
|
||||
final children = <ChatSource>[];
|
||||
|
||||
if (childrenViews != null) {
|
||||
for (final childView in childrenViews) {
|
||||
if (childView.layout == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
if (childView.layout != ViewLayoutPB.Document && hideDisabled) {
|
||||
continue;
|
||||
}
|
||||
final childChatSource = await _recursiveBuild(childView, view);
|
||||
if (childChatSource.selectedStatus.isSelected) {
|
||||
selectedCount++;
|
||||
}
|
||||
children.add(childChatSource);
|
||||
}
|
||||
|
||||
final areAllChildrenSelectedOrNoChildren =
|
||||
children.length == selectedCount;
|
||||
final isAnyChildNotUnselected =
|
||||
children.any((e) => !e.selectedStatus.isUnselected);
|
||||
|
||||
if (isThisSourceSelected && areAllChildrenSelectedOrNoChildren) {
|
||||
selectedStatus = SourceSelectedStatus.selected;
|
||||
} else if (isThisSourceSelected || isAnyChildNotUnselected) {
|
||||
selectedStatus = SourceSelectedStatus.partiallySelected;
|
||||
}
|
||||
} else if (isThisSourceSelected) {
|
||||
selectedStatus = SourceSelectedStatus.selected;
|
||||
}
|
||||
|
||||
return ChatSource(
|
||||
view: view,
|
||||
parentView: parentView,
|
||||
children: children,
|
||||
ignoreStatus: view.layout.isDocumentView
|
||||
? IgnoreViewType.none
|
||||
: IgnoreViewType.disable,
|
||||
isExpanded: false,
|
||||
selectedStatus: selectedStatus,
|
||||
);
|
||||
}
|
||||
|
||||
void _restrictSelectionIfNecessary(List<ChatSource> sources) {
|
||||
for (final source in sources) {
|
||||
source.resetIgnoreViewTypeRecursive();
|
||||
}
|
||||
if (sources.where((e) => !e.selectedStatus.isUnselected).length >=
|
||||
_kMaxSelectedParentPageCount) {
|
||||
sources
|
||||
.where((e) => e.selectedStatus == SourceSelectedStatus.unselected)
|
||||
.forEach(
|
||||
(e) => e.updateIgnoreViewTypeRecursive(IgnoreViewType.disable),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void updateFilter(String filter) {
|
||||
this.filter = filter;
|
||||
for (final source in state.visibleSources) {
|
||||
source.dispose();
|
||||
}
|
||||
if (sources.isEmpty) {
|
||||
emit(ChatSettingsState.initial());
|
||||
} else {
|
||||
final selected =
|
||||
selectedSources.map(_buildSearchResults).nonNulls.toList();
|
||||
final visible =
|
||||
sources.map(_buildSearchResults).nonNulls.nonNulls.toList();
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedSources: selected,
|
||||
visibleSources: visible,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// traverse tree to build up search query
|
||||
ChatSource? _buildSearchResults(ChatSource chatSource) {
|
||||
final isVisible = chatSource.view.nameOrDefault
|
||||
.toLowerCase()
|
||||
.contains(filter.toLowerCase());
|
||||
|
||||
final childrenResults = <ChatSource>[];
|
||||
for (final childSource in chatSource.children) {
|
||||
final childResult = _buildSearchResults(childSource);
|
||||
if (childResult != null) {
|
||||
childrenResults.add(childResult);
|
||||
}
|
||||
}
|
||||
|
||||
return isVisible || childrenResults.isNotEmpty
|
||||
? ChatSource(
|
||||
view: chatSource.view,
|
||||
parentView: chatSource.parentView,
|
||||
children: childrenResults,
|
||||
ignoreStatus: chatSource.ignoreStatus,
|
||||
isExpanded: chatSource.isExpanded,
|
||||
selectedStatus: chatSource.selectedStatus,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
/// traverse tree to build up selected sources
|
||||
Iterable<ChatSource> _buildSelectedSources(ChatSource chatSource) {
|
||||
final children = <ChatSource>[];
|
||||
|
||||
for (final childSource in chatSource.children) {
|
||||
children.addAll(_buildSelectedSources(childSource));
|
||||
}
|
||||
|
||||
return selectedSourceIds.contains(chatSource.view.id)
|
||||
? [
|
||||
ChatSource(
|
||||
view: chatSource.view,
|
||||
parentView: chatSource.parentView,
|
||||
children: children,
|
||||
ignoreStatus: chatSource.ignoreStatus,
|
||||
selectedStatus: chatSource.selectedStatus,
|
||||
isExpanded: true,
|
||||
),
|
||||
]
|
||||
: children;
|
||||
}
|
||||
|
||||
void toggleSelectedStatus(ChatSource chatSource) {
|
||||
if (chatSource.view.isSpace) {
|
||||
return;
|
||||
}
|
||||
final allIds = _recursiveGetSourceIds(chatSource);
|
||||
|
||||
if (chatSource.selectedStatus.isUnselected ||
|
||||
chatSource.selectedStatus.isPartiallySelected &&
|
||||
!chatSource.view.layout.isDocumentView) {
|
||||
for (final id in allIds) {
|
||||
if (!selectedSourceIds.contains(id)) {
|
||||
selectedSourceIds.add(id);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (final id in allIds) {
|
||||
if (selectedSourceIds.contains(id)) {
|
||||
selectedSourceIds.remove(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateSelectedStatus();
|
||||
}
|
||||
|
||||
List<String> _recursiveGetSourceIds(ChatSource chatSource) {
|
||||
return [
|
||||
if (chatSource.view.layout.isDocumentView) chatSource.view.id,
|
||||
for (final childSource in chatSource.children)
|
||||
..._recursiveGetSourceIds(childSource),
|
||||
];
|
||||
}
|
||||
|
||||
void updateSelectedStatus() {
|
||||
if (sources.isEmpty) {
|
||||
return;
|
||||
}
|
||||
for (final source in sources) {
|
||||
_recursiveUpdateSelectedStatus(source);
|
||||
}
|
||||
_restrictSelectionIfNecessary(sources);
|
||||
for (final visibleSource in state.visibleSources) {
|
||||
visibleSource.dispose();
|
||||
}
|
||||
final visible = sources.map(_buildSearchResults).nonNulls.toList();
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
visibleSources: visible,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
SourceSelectedStatus _recursiveUpdateSelectedStatus(ChatSource chatSource) {
|
||||
SourceSelectedStatus selectedStatus = SourceSelectedStatus.unselected;
|
||||
|
||||
int selectedCount = 0;
|
||||
for (final childSource in chatSource.children) {
|
||||
final childStatus = _recursiveUpdateSelectedStatus(childSource);
|
||||
if (childStatus.isSelected) {
|
||||
selectedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
final isThisSourceSelected = selectedSourceIds.contains(chatSource.view.id);
|
||||
final areAllChildrenSelectedOrNoChildren =
|
||||
chatSource.children.length == selectedCount;
|
||||
final isAnyChildNotUnselected =
|
||||
chatSource.children.any((e) => !e.selectedStatus.isUnselected);
|
||||
|
||||
if (isThisSourceSelected && areAllChildrenSelectedOrNoChildren) {
|
||||
selectedStatus = SourceSelectedStatus.selected;
|
||||
} else if (isThisSourceSelected || isAnyChildNotUnselected) {
|
||||
selectedStatus = SourceSelectedStatus.partiallySelected;
|
||||
}
|
||||
|
||||
chatSource.selectedStatusNotifier.value = selectedStatus;
|
||||
return selectedStatus;
|
||||
}
|
||||
|
||||
void toggleIsExpanded(ChatSource chatSource, bool isSelectedSection) {
|
||||
chatSource.toggleIsExpanded();
|
||||
if (isSelectedSection) {
|
||||
for (final selectedSource in selectedSources) {
|
||||
selectedSource
|
||||
.findChildBySourceId(chatSource.view.id)
|
||||
?.toggleIsExpanded();
|
||||
}
|
||||
} else {
|
||||
for (final source in sources) {
|
||||
final child = source.findChildBySourceId(chatSource.view.id);
|
||||
if (child != null) {
|
||||
child.toggleIsExpanded();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
for (final child in sources) {
|
||||
child.dispose();
|
||||
}
|
||||
for (final child in selectedSources) {
|
||||
child.dispose();
|
||||
}
|
||||
for (final child in state.selectedSources) {
|
||||
child.dispose();
|
||||
}
|
||||
for (final child in state.visibleSources) {
|
||||
child.dispose();
|
||||
}
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatSettingsState with _$ChatSettingsState {
|
||||
const factory ChatSettingsState({
|
||||
required List<ChatSource> visibleSources,
|
||||
required List<ChatSource> selectedSources,
|
||||
}) = _ChatSettingsState;
|
||||
|
||||
factory ChatSettingsState.initial() => const ChatSettingsState(
|
||||
visibleSources: [],
|
||||
selectedSources: [],
|
||||
);
|
||||
}
|
||||
@@ -1,16 +1,6 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_message_stream.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_related_question.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/workspace/application/tabs/tabs_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -28,14 +18,20 @@ import 'package:string_validator/string_validator.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import 'application/ai_prompt_input_bloc.dart';
|
||||
import 'application/chat_bloc.dart';
|
||||
import 'application/chat_entity.dart';
|
||||
import 'application/chat_member_bloc.dart';
|
||||
import 'application/chat_message_stream.dart';
|
||||
import 'presentation/animated_chat_list.dart';
|
||||
import 'presentation/chat_input/desktop_ai_prompt_input.dart';
|
||||
import 'presentation/chat_input/mobile_ai_prompt_input.dart';
|
||||
import 'presentation/chat_related_question.dart';
|
||||
import 'presentation/chat_welcome_page.dart';
|
||||
import 'presentation/layout_define.dart';
|
||||
import 'presentation/message/ai_text_message.dart';
|
||||
import 'presentation/message/error_text_message.dart';
|
||||
import 'presentation/message/message_util.dart';
|
||||
import 'presentation/message/user_text_message.dart';
|
||||
import 'presentation/scroll_to_bottom.dart';
|
||||
|
||||
@@ -120,20 +116,13 @@ class _ChatContentPage extends StatelessWidget {
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (context, state) {
|
||||
return state.loadingState.when(
|
||||
loading: () {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator.adaptive(),
|
||||
);
|
||||
},
|
||||
finish: (_) {
|
||||
final chatController =
|
||||
context.read<ChatBloc>().chatController;
|
||||
return Column(
|
||||
return switch (state.loadingState) {
|
||||
LoadChatMessageStatus.ready => Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Chat(
|
||||
chatController: chatController,
|
||||
chatController:
|
||||
context.read<ChatBloc>().chatController,
|
||||
user: User(id: userProfile.id.toString()),
|
||||
darkTheme: ChatTheme.fromThemeData(Theme.of(context)),
|
||||
theme: ChatTheme.fromThemeData(Theme.of(context)),
|
||||
@@ -148,9 +137,9 @@ class _ChatContentPage extends StatelessWidget {
|
||||
),
|
||||
_buildInput(context),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
),
|
||||
_ => const Center(child: CircularProgressIndicator.adaptive()),
|
||||
};
|
||||
},
|
||||
),
|
||||
),
|
||||
@@ -204,14 +193,13 @@ class _ChatContentPage extends StatelessWidget {
|
||||
final refSourceJsonString =
|
||||
message.metadata?[messageRefSourceJsonStringKey] as String?;
|
||||
|
||||
return BlocSelector<ChatBloc, ChatState, bool>(
|
||||
selector: (state) {
|
||||
return BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (context, state) {
|
||||
final chatController = context.read<ChatBloc>().chatController;
|
||||
final messages = chatController.messages
|
||||
.where((e) => onetimeMessageTypeFromMeta(e.metadata) == null);
|
||||
return messages.isEmpty ? false : messages.last.id == message.id;
|
||||
},
|
||||
builder: (context, isLastMessage) {
|
||||
final isLastMessage =
|
||||
messages.isEmpty ? false : messages.last.id == message.id;
|
||||
return ChatAIMessageWidget(
|
||||
user: message.author,
|
||||
messageUserId: message.id,
|
||||
@@ -220,9 +208,16 @@ class _ChatContentPage extends StatelessWidget {
|
||||
questionId: questionId,
|
||||
chatId: view.id,
|
||||
refSourceJsonString: refSourceJsonString,
|
||||
isStreaming: state.promptResponseState != PromptResponseState.ready,
|
||||
isLastMessage: isLastMessage,
|
||||
onSelectedMetadata: (metadata) =>
|
||||
_onSelectMetadata(context, metadata),
|
||||
onRegenerate: () => context
|
||||
.read<ChatBloc>()
|
||||
.add(ChatEvent.regenerateAnswer(message.id, null)),
|
||||
onChangeFormat: (format) => context
|
||||
.read<ChatBloc>()
|
||||
.add(ChatEvent.regenerateAnswer(message.id, format)),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -287,36 +282,54 @@ class _ChatContentPage extends StatelessWidget {
|
||||
return state.promptResponseState == PromptResponseState.ready;
|
||||
},
|
||||
builder: (context, canSendMessage) {
|
||||
final chatBloc = context.read<ChatBloc>();
|
||||
|
||||
return UniversalPlatform.isDesktop
|
||||
? DesktopAIPromptInput(
|
||||
chatId: view.id,
|
||||
onSubmitted: (text, metadata) {
|
||||
context.read<ChatBloc>().add(
|
||||
ChatEvent.sendMessage(
|
||||
message: text,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
},
|
||||
isStreaming: !canSendMessage,
|
||||
onStopStreaming: () => context
|
||||
.read<ChatBloc>()
|
||||
.add(const ChatEvent.stopStream()),
|
||||
onStopStreaming: () {
|
||||
chatBloc.add(const ChatEvent.stopStream());
|
||||
},
|
||||
onSubmitted: (text, format, metadata) {
|
||||
chatBloc.add(
|
||||
ChatEvent.sendMessage(
|
||||
message: text,
|
||||
format: format,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
},
|
||||
onUpdateSelectedSources: (ids) {
|
||||
chatBloc.add(
|
||||
ChatEvent.updateSelectedSources(
|
||||
selectedSourcesIds: ids,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: MobileAIPromptInput(
|
||||
chatId: view.id,
|
||||
onSubmitted: (text, metadata) {
|
||||
context.read<ChatBloc>().add(
|
||||
ChatEvent.sendMessage(
|
||||
message: text,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
},
|
||||
isStreaming: !canSendMessage,
|
||||
onStopStreaming: () => context
|
||||
.read<ChatBloc>()
|
||||
.add(const ChatEvent.stopStream()),
|
||||
onStopStreaming: () {
|
||||
chatBloc.add(const ChatEvent.stopStream());
|
||||
},
|
||||
onSubmitted: (text, format, metadata) {
|
||||
chatBloc.add(
|
||||
ChatEvent.sendMessage(
|
||||
message: text,
|
||||
format: format,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
},
|
||||
onUpdateSelectedSources: (ids) {
|
||||
chatBloc.add(
|
||||
ChatEvent.updateSelectedSources(
|
||||
selectedSourcesIds: ids,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -343,19 +356,8 @@ class _ChatContentPage extends StatelessWidget {
|
||||
} else {
|
||||
final sidebarView =
|
||||
await ViewBackendService.getView(metadata.id).toNullable();
|
||||
if (sidebarView == null) {
|
||||
return;
|
||||
}
|
||||
if (UniversalPlatform.isDesktop) {
|
||||
getIt<TabsBloc>().add(
|
||||
TabsEvent.openSecondaryPlugin(
|
||||
plugin: sidebarView.plugin(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
if (context.mounted) {
|
||||
unawaited(context.pushView(sidebarView));
|
||||
}
|
||||
if (context.mounted) {
|
||||
openPageFromMessage(context, sidebarView);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ class ChatAnimatedListReversedState extends State<ChatAnimatedListReversed>
|
||||
event.message != null,
|
||||
'Message must be provided when inserting a message.',
|
||||
);
|
||||
_onInserted(0, event.message!);
|
||||
_onInserted(event.index!, event.message!);
|
||||
_oldList = List.from(_chatController.messages);
|
||||
break;
|
||||
case ChatOperationType.remove:
|
||||
@@ -208,7 +208,7 @@ class ChatAnimatedListReversedState extends State<ChatAnimatedListReversed>
|
||||
if (data.id == _lastInsertedMessageId &&
|
||||
widget.scrollController.offset >
|
||||
widget.scrollController.position.minScrollExtent &&
|
||||
(user.id == data.author.id || !_userHasScrolled)) {
|
||||
(user.id == data.author.id && _userHasScrolled)) {
|
||||
if (widget.scrollToEndAnimationDuration == Duration.zero) {
|
||||
widget.scrollController
|
||||
.jumpTo(widget.scrollController.position.minScrollExtent);
|
||||
@@ -234,7 +234,7 @@ class ChatAnimatedListReversedState extends State<ChatAnimatedListReversed>
|
||||
if (data.id == _lastInsertedMessageId &&
|
||||
widget.scrollController.offset >
|
||||
widget.scrollController.position.minScrollExtent &&
|
||||
(user.id == data.author.id || !_userHasScrolled)) {
|
||||
(user.id == data.author.id && _userHasScrolled)) {
|
||||
widget.scrollController
|
||||
.jumpTo(widget.scrollController.position.minScrollExtent);
|
||||
}
|
||||
@@ -311,14 +311,15 @@ class ChatAnimatedListReversedState extends State<ChatAnimatedListReversed>
|
||||
// If for some reason `_userHasScrolled` is true and the user is not at the
|
||||
// bottom of the list, set `_userHasScrolled` to false so that the scroll
|
||||
// animation is triggered.
|
||||
if (_userHasScrolled &&
|
||||
widget.scrollController.offset >=
|
||||
if (position == 0 &&
|
||||
_userHasScrolled &&
|
||||
widget.scrollController.offset >
|
||||
widget.scrollController.position.minScrollExtent) {
|
||||
_userHasScrolled = false;
|
||||
}
|
||||
|
||||
_listKey.currentState!.insertItem(
|
||||
position,
|
||||
0,
|
||||
duration: widget.insertAnimationDuration,
|
||||
);
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class ChatEditorStyleCustomizer extends EditorStyleCustomizer {
|
||||
DefaultAppearanceSettings.getDefaultSelectionColor(context),
|
||||
defaultTextDirection: appearance.defaultTextDirection,
|
||||
textStyleConfiguration: TextStyleConfiguration(
|
||||
lineHeight: 1.4,
|
||||
lineHeight: 20 / 14,
|
||||
applyHeightToFirstAscent: true,
|
||||
applyHeightToLastDescent: true,
|
||||
text: baseTextStyle(fontFamily).copyWith(
|
||||
|
||||
+5
-4
@@ -1,6 +1,8 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view_title/view_title_bar_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
|
||||
@@ -308,10 +310,9 @@ class MentionViewIcon extends StatelessWidget {
|
||||
if (view.icon.value.isNotEmpty) {
|
||||
return SizedBox(
|
||||
width: 16.0,
|
||||
child: FlowyText.emoji(
|
||||
view.icon.value,
|
||||
fontSize: 14.0,
|
||||
figmaLineHeight: 21.0,
|
||||
child: RawEmojiIconWidget(
|
||||
emoji: view.icon.toEmojiIconData(),
|
||||
emojiSize: 14,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
+170
-63
@@ -10,13 +10,15 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'chat_mention_page_menu.dart';
|
||||
import '../../application/chat_entity.dart';
|
||||
import '../layout_define.dart';
|
||||
import 'ai_prompt_buttons.dart';
|
||||
import 'chat_input_file.dart';
|
||||
import 'chat_input_span.dart';
|
||||
import 'chat_mention_page_menu.dart';
|
||||
import 'predefined_format_buttons.dart';
|
||||
import 'select_sources_menu.dart';
|
||||
|
||||
class DesktopAIPromptInput extends StatefulWidget {
|
||||
const DesktopAIPromptInput({
|
||||
@@ -25,12 +27,15 @@ class DesktopAIPromptInput extends StatefulWidget {
|
||||
required this.isStreaming,
|
||||
required this.onStopStreaming,
|
||||
required this.onSubmitted,
|
||||
required this.onUpdateSelectedSources,
|
||||
});
|
||||
|
||||
final String chatId;
|
||||
final bool isStreaming;
|
||||
final void Function() onStopStreaming;
|
||||
final void Function(String, Map<String, dynamic>) onSubmitted;
|
||||
final void Function(String, PredefinedFormat?, Map<String, dynamic>)
|
||||
onSubmitted;
|
||||
final void Function(List<String>) onUpdateSelectedSources;
|
||||
|
||||
@override
|
||||
State<DesktopAIPromptInput> createState() => _DesktopAIPromptInputState();
|
||||
@@ -44,6 +49,11 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
final focusNode = FocusNode();
|
||||
final textController = TextEditingController();
|
||||
|
||||
bool showPredefinedFormatSection = true;
|
||||
PredefinedFormat predefinedFormat = const PredefinedFormat(
|
||||
imageFormat: ImageFormat.text,
|
||||
textFormat: TextFormat.bulletList,
|
||||
);
|
||||
late SendButtonState sendButtonState;
|
||||
|
||||
@override
|
||||
@@ -112,6 +122,8 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
color: focusNode.hasFocus
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.outline,
|
||||
width: focusNode.hasFocus ? 1.5 : 1.0,
|
||||
strokeAlign: BorderSide.strokeAlignOutside,
|
||||
),
|
||||
borderRadius: DesktopAIPromptSizes.promptFrameRadius,
|
||||
),
|
||||
@@ -133,16 +145,35 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const VSpace(4.0),
|
||||
Stack(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: DesktopAIPromptSizes.textFieldMinHeight +
|
||||
DesktopAIPromptSizes.actionBarHeight,
|
||||
maxHeight: 300,
|
||||
),
|
||||
Container(
|
||||
constraints: getTextFieldConstraints(),
|
||||
child: inputTextField(),
|
||||
),
|
||||
if (showPredefinedFormatSection)
|
||||
Positioned.fill(
|
||||
bottom: null,
|
||||
child: TextFieldTapRegion(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsetsDirectional.only(start: 8.0),
|
||||
child: ChangeFormatBar(
|
||||
predefinedFormat: predefinedFormat,
|
||||
spacing: DesktopAIPromptSizes
|
||||
.predefinedFormatBarButtonSpacing,
|
||||
iconSize: DesktopAIPromptSizes
|
||||
.predefinedFormatIconHeight,
|
||||
buttonSize: DesktopAIPromptSizes
|
||||
.predefinedFormatButtonHeight,
|
||||
onSelectPredefinedFormat: (format) {
|
||||
setState(() => predefinedFormat = format);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned.fill(
|
||||
top: null,
|
||||
child: TextFieldTapRegion(
|
||||
@@ -150,9 +181,20 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
textController: textController,
|
||||
overlayController: overlayController,
|
||||
focusNode: focusNode,
|
||||
showPredefinedFormats: showPredefinedFormatSection,
|
||||
predefinedFormat: predefinedFormat.imageFormat,
|
||||
predefinedTextFormat: predefinedFormat.textFormat,
|
||||
onTogglePredefinedFormatSection: () {
|
||||
setState(() {
|
||||
showPredefinedFormatSection =
|
||||
!showPredefinedFormatSection;
|
||||
});
|
||||
},
|
||||
sendButtonState: sendButtonState,
|
||||
onSendPressed: handleSendPressed,
|
||||
onStopStreaming: widget.onStopStreaming,
|
||||
onUpdateSelectedSources:
|
||||
widget.onUpdateSelectedSources,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -166,6 +208,18 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
);
|
||||
}
|
||||
|
||||
BoxConstraints getTextFieldConstraints() {
|
||||
double minHeight = DesktopAIPromptSizes.textFieldMinHeight +
|
||||
DesktopAIPromptSizes.actionBarHeight +
|
||||
DesktopAIPromptSizes.actionBarPadding.vertical;
|
||||
double maxHeight = 300;
|
||||
if (showPredefinedFormatSection) {
|
||||
minHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;
|
||||
maxHeight += DesktopAIPromptSizes.predefinedFormatButtonHeight;
|
||||
}
|
||||
return BoxConstraints(minHeight: minHeight, maxHeight: maxHeight);
|
||||
}
|
||||
|
||||
void cancelMentionPage() {
|
||||
if (overlayController.isShowing) {
|
||||
inputControlCubit.reset();
|
||||
@@ -184,6 +238,9 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
}
|
||||
|
||||
void handleSendPressed() {
|
||||
if (widget.isStreaming) {
|
||||
return;
|
||||
}
|
||||
final trimmedText = inputControlCubit.formatIntputText(
|
||||
textController.text.trim(),
|
||||
);
|
||||
@@ -195,7 +252,11 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
// get the attached files and mentioned pages
|
||||
final metadata = context.read<AIPromptInputBloc>().consumeMetadata();
|
||||
|
||||
widget.onSubmitted(trimmedText, metadata);
|
||||
widget.onSubmitted(
|
||||
trimmedText,
|
||||
showPredefinedFormatSection ? predefinedFormat : null,
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
void handleTextControllerChanged() {
|
||||
@@ -207,6 +268,10 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
setState(() => updateSendButtonState());
|
||||
|
||||
// handle text and selection changes ONLY when mentioning a page
|
||||
|
||||
// disable mention
|
||||
return;
|
||||
// ignore: dead_code
|
||||
if (!overlayController.isShowing ||
|
||||
inputControlCubit.filterStartPosition == -1) {
|
||||
return;
|
||||
@@ -288,16 +353,17 @@ class _DesktopAIPromptInputState extends State<DesktopAIPromptInput> {
|
||||
cubit: inputControlCubit,
|
||||
textController: textController,
|
||||
textFieldFocusNode: focusNode,
|
||||
showPredefinedFormatSection: showPredefinedFormatSection,
|
||||
hintText: switch (state.aiType) {
|
||||
AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(),
|
||||
AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr()
|
||||
},
|
||||
onStartMentioningPage: () {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
inputControlCubit.startSearching(textController.value);
|
||||
overlayController.show();
|
||||
});
|
||||
},
|
||||
// onStartMentioningPage: () {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// inputControlCubit.startSearching(textController.value);
|
||||
// overlayController.show();
|
||||
// });
|
||||
// },
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -353,15 +419,17 @@ class _PromptTextField extends StatefulWidget {
|
||||
required this.cubit,
|
||||
required this.textController,
|
||||
required this.textFieldFocusNode,
|
||||
required this.onStartMentioningPage,
|
||||
this.showPredefinedFormatSection = false,
|
||||
this.hintText = "",
|
||||
// this.onStartMentioningPage,
|
||||
});
|
||||
|
||||
final ChatInputControlCubit cubit;
|
||||
final TextEditingController textController;
|
||||
final FocusNode textFieldFocusNode;
|
||||
final void Function() onStartMentioningPage;
|
||||
final bool showPredefinedFormatSection;
|
||||
final String hintText;
|
||||
// final void Function()? onStartMentioningPage;
|
||||
|
||||
@override
|
||||
State<_PromptTextField> createState() => _PromptTextFieldState();
|
||||
@@ -373,13 +441,13 @@ class _PromptTextFieldState extends State<_PromptTextField> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
widget.textFieldFocusNode.onKeyEvent = handleKeyEvent;
|
||||
// widget.textFieldFocusNode.onKeyEvent = handleKeyEvent;
|
||||
widget.textController.addListener(onTextChanged);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.textFieldFocusNode.onKeyEvent = null;
|
||||
// widget.textFieldFocusNode.onKeyEvent = null;
|
||||
widget.textController.removeListener(onTextChanged);
|
||||
super.dispose();
|
||||
}
|
||||
@@ -395,16 +463,9 @@ class _PromptTextFieldState extends State<_PromptTextField> {
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: DesktopAIPromptSizes.textFieldContentPadding.add(
|
||||
const EdgeInsets.only(
|
||||
bottom: DesktopAIPromptSizes.actionBarHeight,
|
||||
),
|
||||
),
|
||||
contentPadding: calculateContentPadding(),
|
||||
hintText: widget.hintText,
|
||||
hintStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium
|
||||
?.copyWith(color: Theme.of(context).hintColor),
|
||||
hintStyle: AIChatUILayout.inputHintTextStyle(context),
|
||||
isCollapsed: true,
|
||||
isDense: true,
|
||||
),
|
||||
@@ -424,12 +485,12 @@ class _PromptTextFieldState extends State<_PromptTextField> {
|
||||
);
|
||||
}
|
||||
|
||||
KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (event.character == '@') {
|
||||
widget.onStartMentioningPage();
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
// KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) {
|
||||
// if (event.character == '@') {
|
||||
// widget.onStartMentioningPage();
|
||||
// }
|
||||
// return KeyEventResult.ignored;
|
||||
// }
|
||||
|
||||
void onTextChanged() {
|
||||
setState(
|
||||
@@ -437,6 +498,16 @@ class _PromptTextFieldState extends State<_PromptTextField> {
|
||||
);
|
||||
}
|
||||
|
||||
EdgeInsetsGeometry calculateContentPadding() {
|
||||
final top = widget.showPredefinedFormatSection
|
||||
? DesktopAIPromptSizes.predefinedFormatButtonHeight
|
||||
: 0.0;
|
||||
const bottom = DesktopAIPromptSizes.actionBarHeight;
|
||||
|
||||
return DesktopAIPromptSizes.textFieldContentPadding
|
||||
.add(EdgeInsets.only(top: top, bottom: bottom));
|
||||
}
|
||||
|
||||
Map<ShortcutActivator, Intent> buildShortcuts() {
|
||||
if (isComposing) {
|
||||
return const {};
|
||||
@@ -473,34 +544,55 @@ class _PromptBottomActions extends StatelessWidget {
|
||||
required this.overlayController,
|
||||
required this.focusNode,
|
||||
required this.sendButtonState,
|
||||
required this.predefinedFormat,
|
||||
required this.predefinedTextFormat,
|
||||
required this.onTogglePredefinedFormatSection,
|
||||
required this.showPredefinedFormats,
|
||||
required this.onSendPressed,
|
||||
required this.onStopStreaming,
|
||||
required this.onUpdateSelectedSources,
|
||||
});
|
||||
|
||||
final TextEditingController textController;
|
||||
final OverlayPortalController overlayController;
|
||||
final FocusNode focusNode;
|
||||
final bool showPredefinedFormats;
|
||||
final ImageFormat predefinedFormat;
|
||||
final TextFormat? predefinedTextFormat;
|
||||
final void Function() onTogglePredefinedFormatSection;
|
||||
final SendButtonState sendButtonState;
|
||||
final void Function() onSendPressed;
|
||||
final void Function() onStopStreaming;
|
||||
final void Function(List<String>) onUpdateSelectedSources;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: DesktopAIPromptSizes.actionBarHeight,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
margin: DesktopAIPromptSizes.actionBarPadding,
|
||||
child: BlocBuilder<AIPromptInputBloc, AIPromptInputState>(
|
||||
builder: (context, state) {
|
||||
if (state.chatState == null) {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
child: _sendButton(),
|
||||
);
|
||||
}
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// predefinedFormatButton(),
|
||||
_predefinedFormatButton(),
|
||||
const Spacer(),
|
||||
_mentionButton(context),
|
||||
const HSpace(
|
||||
DesktopAIPromptSizes.actionBarButtonSpacing,
|
||||
),
|
||||
if (UniversalPlatform.isDesktop && state.supportChatWithFile) ...[
|
||||
if (state.aiType == AIType.appflowyAI) ...[
|
||||
_selectSourcesButton(context),
|
||||
const HSpace(
|
||||
DesktopAIPromptSizes.actionBarButtonSpacing,
|
||||
),
|
||||
],
|
||||
// _mentionButton(context),
|
||||
// const HSpace(
|
||||
// DesktopAIPromptSizes.actionBarButtonSpacing,
|
||||
// ),
|
||||
if (state.supportChatWithFile) ...[
|
||||
_attachmentButton(context),
|
||||
const HSpace(
|
||||
DesktopAIPromptSizes.actionBarButtonSpacing,
|
||||
@@ -514,28 +606,43 @@ class _PromptBottomActions extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _mentionButton(BuildContext context) {
|
||||
return PromptInputMentionButton(
|
||||
iconSize: DesktopAIPromptSizes.actionBarIconSize,
|
||||
buttonSize: DesktopAIPromptSizes.actionBarButtonSize,
|
||||
onTap: () {
|
||||
if (overlayController.isShowing) {
|
||||
return;
|
||||
}
|
||||
if (!focusNode.hasFocus) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
textController.text += '@';
|
||||
Future.delayed(Duration.zero, () {
|
||||
context
|
||||
.read<ChatInputControlCubit>()
|
||||
.startSearching(textController.value);
|
||||
overlayController.show();
|
||||
});
|
||||
},
|
||||
Widget _predefinedFormatButton() {
|
||||
return PromptInputDesktopToggleFormatButton(
|
||||
showFormatBar: showPredefinedFormats,
|
||||
predefinedFormat: predefinedFormat,
|
||||
predefinedTextFormat: predefinedTextFormat,
|
||||
onTap: onTogglePredefinedFormatSection,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _selectSourcesButton(BuildContext context) {
|
||||
return PromptInputDesktopSelectSourcesButton(
|
||||
onUpdateSelectedSources: onUpdateSelectedSources,
|
||||
);
|
||||
}
|
||||
|
||||
// Widget _mentionButton(BuildContext context) {
|
||||
// return PromptInputMentionButton(
|
||||
// iconSize: DesktopAIPromptSizes.actionBarIconSize,
|
||||
// buttonSize: DesktopAIPromptSizes.actionBarButtonSize,
|
||||
// onTap: () {
|
||||
// if (overlayController.isShowing) {
|
||||
// return;
|
||||
// }
|
||||
// if (!focusNode.hasFocus) {
|
||||
// focusNode.requestFocus();
|
||||
// }
|
||||
// textController.text += '@';
|
||||
// Future.delayed(Duration.zero, () {
|
||||
// context
|
||||
// .read<ChatInputControlCubit>()
|
||||
// .startSearching(textController.value);
|
||||
// overlayController.show();
|
||||
// });
|
||||
// },
|
||||
// );
|
||||
// }
|
||||
|
||||
Widget _attachmentButton(BuildContext context) {
|
||||
return PromptInputAttachmentButton(
|
||||
onTap: () async {
|
||||
@@ -562,7 +669,7 @@ class _PromptBottomActions extends StatelessWidget {
|
||||
|
||||
Widget _sendButton() {
|
||||
return PromptInputSendButton(
|
||||
buttonSize: DesktopAIPromptSizes.actionBarButtonSize,
|
||||
buttonSize: DesktopAIPromptSizes.actionBarHeight,
|
||||
iconSize: DesktopAIPromptSizes.sendButtonSize,
|
||||
state: sendButtonState,
|
||||
onSendPressed: onSendPressed,
|
||||
|
||||
+114
-43
@@ -1,5 +1,6 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/ai_prompt_input_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
@@ -13,6 +14,8 @@ import 'ai_prompt_buttons.dart';
|
||||
import 'chat_input_file.dart';
|
||||
import 'chat_input_span.dart';
|
||||
import 'chat_mention_page_bottom_sheet.dart';
|
||||
import 'predefined_format_buttons.dart';
|
||||
import 'select_sources_bottom_sheet.dart';
|
||||
|
||||
class MobileAIPromptInput extends StatefulWidget {
|
||||
const MobileAIPromptInput({
|
||||
@@ -21,12 +24,15 @@ class MobileAIPromptInput extends StatefulWidget {
|
||||
required this.isStreaming,
|
||||
required this.onStopStreaming,
|
||||
required this.onSubmitted,
|
||||
required this.onUpdateSelectedSources,
|
||||
});
|
||||
|
||||
final String chatId;
|
||||
final bool isStreaming;
|
||||
final void Function() onStopStreaming;
|
||||
final void Function(String, Map<String, dynamic>) onSubmitted;
|
||||
final void Function(String, PredefinedFormat?, Map<String, dynamic>)
|
||||
onSubmitted;
|
||||
final void Function(List<String>) onUpdateSelectedSources;
|
||||
|
||||
@override
|
||||
State<MobileAIPromptInput> createState() => _MobileAIPromptInputState();
|
||||
@@ -37,6 +43,11 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
final focusNode = FocusNode();
|
||||
final textController = TextEditingController();
|
||||
|
||||
bool showPredefinedFormatSection = true;
|
||||
PredefinedFormat predefinedFormat = const PredefinedFormat(
|
||||
imageFormat: ImageFormat.text,
|
||||
textFormat: TextFormat.bulletList,
|
||||
);
|
||||
late SendButtonState sendButtonState;
|
||||
|
||||
@override
|
||||
@@ -44,7 +55,7 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
super.initState();
|
||||
|
||||
textController.addListener(handleTextControllerChange);
|
||||
focusNode.onKeyEvent = handleKeyEvent;
|
||||
// focusNode.onKeyEvent = handleKeyEvent;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
focusNode.requestFocus();
|
||||
@@ -100,6 +111,7 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
borderRadius: MobileAIPromptSizes.promptFrameRadius,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
@@ -114,24 +126,33 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
.add(AIPromptInputEvent.removeFile(file)),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
constraints: const BoxConstraints(
|
||||
minHeight: MobileAIPromptSizes.textFieldMinHeight,
|
||||
maxHeight: 220,
|
||||
),
|
||||
padding:
|
||||
const EdgeInsetsDirectional.fromSTEB(0, 8.0, 12.0, 8.0),
|
||||
child: IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
const HSpace(8.0),
|
||||
Expanded(child: inputTextField(context)),
|
||||
mentionButton(),
|
||||
const HSpace(6.0),
|
||||
sendButton(),
|
||||
],
|
||||
if (showPredefinedFormatSection)
|
||||
TextFieldTapRegion(
|
||||
child: Container(
|
||||
padding: MobileAIPromptSizes.predefinedFormatBarPadding,
|
||||
child: ChangeFormatBar(
|
||||
predefinedFormat: predefinedFormat,
|
||||
spacing: MobileAIPromptSizes
|
||||
.predefinedFormatBarButtonSpacing,
|
||||
iconSize:
|
||||
MobileAIPromptSizes.predefinedFormatIconHeight,
|
||||
buttonSize:
|
||||
MobileAIPromptSizes.predefinedFormatButtonHeight,
|
||||
onSelectPredefinedFormat: (format) {
|
||||
setState(() => predefinedFormat = format);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
inputTextField(context),
|
||||
Row(
|
||||
children: [
|
||||
const HSpace(8.0),
|
||||
leadingButtons(context),
|
||||
const Spacer(),
|
||||
sendButton(),
|
||||
const HSpace(12.0),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -152,6 +173,9 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
}
|
||||
|
||||
void handleSendPressed() {
|
||||
if (widget.isStreaming) {
|
||||
return;
|
||||
}
|
||||
final trimmedText = inputControlCubit.formatIntputText(
|
||||
textController.text.trim(),
|
||||
);
|
||||
@@ -163,25 +187,29 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
// get the attached files and mentioned pages
|
||||
final metadata = context.read<AIPromptInputBloc>().consumeMetadata();
|
||||
|
||||
widget.onSubmitted(trimmedText, metadata);
|
||||
widget.onSubmitted(
|
||||
trimmedText,
|
||||
showPredefinedFormatSection ? predefinedFormat : null,
|
||||
metadata,
|
||||
);
|
||||
}
|
||||
|
||||
void handleTextControllerChange() {
|
||||
if (textController.value.isComposingRangeValid) {
|
||||
return;
|
||||
}
|
||||
inputControlCubit.updateInputText(textController.text);
|
||||
// inputControlCubit.updateInputText(textController.text);
|
||||
setState(() => updateSendButtonState());
|
||||
}
|
||||
|
||||
KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) {
|
||||
if (event.character == '@') {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
mentionPage(context);
|
||||
});
|
||||
}
|
||||
return KeyEventResult.ignored;
|
||||
}
|
||||
// KeyEventResult handleKeyEvent(FocusNode node, KeyEvent event) {
|
||||
// if (event.character == '@') {
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// mentionPage(context);
|
||||
// });
|
||||
// }
|
||||
// return KeyEventResult.ignored;
|
||||
// }
|
||||
|
||||
Future<void> mentionPage(BuildContext context) async {
|
||||
// if the focus node is on focus, unfocus it for better animation
|
||||
@@ -230,6 +258,7 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
return ExtendedTextField(
|
||||
controller: textController,
|
||||
focusNode: focusNode,
|
||||
textAlignVertical: TextAlignVertical.center,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
@@ -239,6 +268,7 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
AIType.appflowyAI => LocaleKeys.chat_inputMessageHint.tr(),
|
||||
AIType.localAI => LocaleKeys.chat_inputLocalAIMessageHint.tr()
|
||||
},
|
||||
hintStyle: AIChatUILayout.inputHintTextStyle(context),
|
||||
isCollapsed: true,
|
||||
isDense: true,
|
||||
),
|
||||
@@ -246,7 +276,8 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
minLines: 1,
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium?.copyWith(height: 20 / 14),
|
||||
specialTextSpanBuilder: ChatInputTextSpanBuilder(
|
||||
inputControlCubit: inputControlCubit,
|
||||
specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
@@ -259,27 +290,33 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget mentionButton() {
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: PromptInputMentionButton(
|
||||
iconSize: MobileAIPromptSizes.mentionIconSize,
|
||||
buttonSize: MobileAIPromptSizes.sendButtonSize,
|
||||
onTap: () {
|
||||
textController.text += '@';
|
||||
if (!focusNode.hasFocus) {
|
||||
focusNode.requestFocus();
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
mentionPage(context);
|
||||
Widget leadingButtons(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: _LeadingActions(
|
||||
// onMention: () {
|
||||
// textController.text += '@';
|
||||
// if (!focusNode.hasFocus) {
|
||||
// focusNode.requestFocus();
|
||||
// }
|
||||
// WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
// mentionPage(context);
|
||||
// });
|
||||
// },
|
||||
showPredefinedFormatSection: showPredefinedFormatSection,
|
||||
predefinedFormat: predefinedFormat,
|
||||
onTogglePredefinedFormatSection: () {
|
||||
setState(() {
|
||||
showPredefinedFormatSection = !showPredefinedFormatSection;
|
||||
});
|
||||
},
|
||||
onUpdateSelectedSources: widget.onUpdateSelectedSources,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget sendButton() {
|
||||
return Align(
|
||||
return Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: PromptInputSendButton(
|
||||
buttonSize: MobileAIPromptSizes.sendButtonSize,
|
||||
@@ -291,3 +328,37 @@ class _MobileAIPromptInputState extends State<MobileAIPromptInput> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LeadingActions extends StatelessWidget {
|
||||
const _LeadingActions({
|
||||
required this.showPredefinedFormatSection,
|
||||
required this.predefinedFormat,
|
||||
required this.onTogglePredefinedFormatSection,
|
||||
required this.onUpdateSelectedSources,
|
||||
});
|
||||
|
||||
final bool showPredefinedFormatSection;
|
||||
final PredefinedFormat predefinedFormat;
|
||||
final void Function() onTogglePredefinedFormatSection;
|
||||
final void Function(List<String>) onUpdateSelectedSources;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SeparatedRow(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
separatorBuilder: () => const HSpace(4.0),
|
||||
children: [
|
||||
PromptInputMobileSelectSourcesButton(
|
||||
onUpdateSelectedSources: onUpdateSelectedSources,
|
||||
),
|
||||
PromptInputMobileToggleFormatButton(
|
||||
showFormatBar: showPredefinedFormatSection,
|
||||
onTap: onTogglePredefinedFormatSection,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
+202
@@ -0,0 +1,202 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/hover.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import '../layout_define.dart';
|
||||
|
||||
class PromptInputDesktopToggleFormatButton extends StatelessWidget {
|
||||
const PromptInputDesktopToggleFormatButton({
|
||||
super.key,
|
||||
required this.showFormatBar,
|
||||
required this.predefinedFormat,
|
||||
required this.predefinedTextFormat,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool showFormatBar;
|
||||
final ImageFormat predefinedFormat;
|
||||
final TextFormat? predefinedTextFormat;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
tooltipText: showFormatBar
|
||||
? LocaleKeys.chat_changeFormat_defaultDescription.tr()
|
||||
: LocaleKeys.chat_changeFormat_blankDescription.tr(),
|
||||
width: 28.0,
|
||||
onPressed: onTap,
|
||||
icon: showFormatBar
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_aa_text_s,
|
||||
size: Size.square(16.0),
|
||||
color: Color(0xFF666D76),
|
||||
)
|
||||
: const FlowySvg(
|
||||
FlowySvgs.ai_text_image_s,
|
||||
size: Size(21.0, 16.0),
|
||||
color: Color(0xFF666D76),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ChangeFormatBar extends StatelessWidget {
|
||||
const ChangeFormatBar({
|
||||
super.key,
|
||||
required this.predefinedFormat,
|
||||
required this.buttonSize,
|
||||
required this.iconSize,
|
||||
required this.spacing,
|
||||
required this.onSelectPredefinedFormat,
|
||||
});
|
||||
|
||||
final PredefinedFormat? predefinedFormat;
|
||||
final double buttonSize;
|
||||
final double iconSize;
|
||||
final double spacing;
|
||||
final void Function(PredefinedFormat) onSelectPredefinedFormat;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox(
|
||||
height: DesktopAIPromptSizes.predefinedFormatButtonHeight,
|
||||
child: SeparatedRow(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
separatorBuilder: () => HSpace(spacing),
|
||||
children: [
|
||||
_buildFormatButton(context, ImageFormat.text),
|
||||
_buildFormatButton(context, ImageFormat.textAndImage),
|
||||
_buildFormatButton(context, ImageFormat.image),
|
||||
if (predefinedFormat?.imageFormat.hasText ?? true) ...[
|
||||
_buildDivider(),
|
||||
_buildTextFormatButton(context, TextFormat.auto),
|
||||
_buildTextFormatButton(context, TextFormat.bulletList),
|
||||
_buildTextFormatButton(context, TextFormat.numberedList),
|
||||
_buildTextFormatButton(context, TextFormat.table),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildFormatButton(BuildContext context, ImageFormat format) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (predefinedFormat != null &&
|
||||
format == predefinedFormat!.imageFormat) {
|
||||
return;
|
||||
}
|
||||
if (format.hasText) {
|
||||
final textFormat = predefinedFormat?.textFormat ?? TextFormat.auto;
|
||||
onSelectPredefinedFormat(
|
||||
PredefinedFormat(imageFormat: format, textFormat: textFormat),
|
||||
);
|
||||
} else {
|
||||
onSelectPredefinedFormat(
|
||||
PredefinedFormat(imageFormat: format, textFormat: null),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: FlowyTooltip(
|
||||
message: format.i18n,
|
||||
child: SizedBox.square(
|
||||
dimension: buttonSize,
|
||||
child: FlowyHover(
|
||||
isSelected: () => format == predefinedFormat?.imageFormat,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
format.icon,
|
||||
size: format == ImageFormat.textAndImage
|
||||
? Size(21.0 / 16.0 * iconSize, iconSize)
|
||||
: Size.square(iconSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDivider() {
|
||||
return VerticalDivider(
|
||||
indent: 6.0,
|
||||
endIndent: 6.0,
|
||||
width: 1.0 + spacing * 2,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTextFormatButton(
|
||||
BuildContext context,
|
||||
TextFormat format,
|
||||
) {
|
||||
return GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
if (predefinedFormat != null &&
|
||||
format == predefinedFormat!.textFormat) {
|
||||
return;
|
||||
}
|
||||
onSelectPredefinedFormat(
|
||||
PredefinedFormat(
|
||||
imageFormat: predefinedFormat?.imageFormat ?? ImageFormat.text,
|
||||
textFormat: format,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: FlowyTooltip(
|
||||
message: format.i18n,
|
||||
child: SizedBox.square(
|
||||
dimension: buttonSize,
|
||||
child: FlowyHover(
|
||||
isSelected: () => format == predefinedFormat?.textFormat,
|
||||
child: Center(
|
||||
child: FlowySvg(
|
||||
format.icon,
|
||||
size: Size.square(iconSize),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PromptInputMobileToggleFormatButton extends StatelessWidget {
|
||||
const PromptInputMobileToggleFormatButton({
|
||||
super.key,
|
||||
required this.showFormatBar,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool showFormatBar;
|
||||
final VoidCallback onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SizedBox.square(
|
||||
dimension: 32.0,
|
||||
child: FlowyButton(
|
||||
radius: const BorderRadius.all(Radius.circular(8.0)),
|
||||
margin: EdgeInsets.zero,
|
||||
expandText: false,
|
||||
text: showFormatBar
|
||||
? const FlowySvg(
|
||||
FlowySvgs.m_aa_text_s,
|
||||
size: Size.square(20.0),
|
||||
)
|
||||
: const FlowySvg(
|
||||
FlowySvgs.ai_text_image_s,
|
||||
size: Size(26.25, 20.0),
|
||||
),
|
||||
onTap: onTap,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Loaded 100 of 3055 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user