Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
627b8ab43c | ||
|
|
84d55b5022 | ||
|
|
b53e20f746 | ||
|
|
cd06161dea | ||
|
|
776f70a0c7 | ||
|
|
1d1647d58d | ||
|
|
b3f94be33f | ||
|
|
ace398537b | ||
|
|
430e21aec2 | ||
|
|
0c1eb7306a | ||
|
|
db11886e5f | ||
|
|
0d13336b32 | ||
|
|
60ad397105 | ||
|
|
fca3189c97 | ||
|
|
e63f767926 | ||
|
|
8826e479eb | ||
|
|
8e4fe3d559 | ||
|
|
e2ee11e48a | ||
|
|
bdc0fa1f2a | ||
|
|
3836545682 | ||
|
|
02eb0e0b83 |
No files matched your search
@@ -7,6 +7,7 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
|
||||
pull_request:
|
||||
branches:
|
||||
@@ -19,7 +20,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
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.81.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
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,6 +7,7 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
pull_request:
|
||||
@@ -15,11 +16,12 @@ on:
|
||||
paths:
|
||||
- ".github/workflows/mobile_ci.yaml"
|
||||
- "frontend/**"
|
||||
- "!frontend/appflowy_tauri/**"
|
||||
- "!frontend/appflowy_web_app/**"
|
||||
|
||||
env:
|
||||
FLUTTER_VERSION: "3.22.3"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
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.81.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
create-release:
|
||||
@@ -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.81.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
ubuntu-job:
|
||||
|
||||
@@ -11,7 +11,7 @@ on:
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
FLUTTER_VERSION: "3.22.0"
|
||||
RUST_TOOLCHAIN: "1.81.0"
|
||||
RUST_TOOLCHAIN: "1.80.1"
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
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"
|
||||
@@ -0,0 +1,111 @@
|
||||
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"
|
||||
@@ -0,0 +1,152 @@
|
||||
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
|
||||
@@ -0,0 +1,75 @@
|
||||
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
|
||||
@@ -0,0 +1,65 @@
|
||||
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
|
||||
|
||||
+13
-37
@@ -1,43 +1,19 @@
|
||||
# Release Notes
|
||||
## Version 0.8.2 - 23/01/2025
|
||||
### New Features
|
||||
- Customized database view icons
|
||||
- Support for uploading images as custom icons
|
||||
- Enabled selecting multiple AI messages to save into a document
|
||||
- Added the ability to scale the app's display size on mobile
|
||||
- Support for pasting image links without file extensions
|
||||
### Bug Fixes
|
||||
- Fixed an issue where pasting tables from other apps wasn't working
|
||||
- Fixed homepage URL issues in Settings
|
||||
- Fixed an issue where the 'Cancel' button was not visible on the Shortcuts page
|
||||
|
||||
## 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
|
||||
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
|
||||
|
||||
Vendored
+138
-123
@@ -1,125 +1,140 @@
|
||||
{
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "request": "launch",
|
||||
// "program": "[YOUR_APPLICATION_PATH]",
|
||||
},
|
||||
]
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
// This task only builds the Dart code of AppFlowy.
|
||||
// It supports both the desktop and mobile version.
|
||||
"name": "AF: Build Dart Only",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"env": {
|
||||
"RUST_LOG": "debug",
|
||||
},
|
||||
// uncomment the following line to testing performance.
|
||||
// "flutterMode": "profile",
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core",
|
||||
"env": {
|
||||
"RUST_LOG": "trace",
|
||||
"RUST_BACKTRACE": "1"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
// This task builds will:
|
||||
// - call the clean task,
|
||||
// - rebuild all the generated Files (including freeze and language files)
|
||||
// - rebuild the the Rust and Dart code of AppFlowy.
|
||||
"name": "AF-desktop: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For iOS Simulator",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-iOS-Simulator: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (iOS Simulator)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Build All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Build Appflowy Core For Android",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-Android: Clean + Rebuild All",
|
||||
"request": "launch",
|
||||
"program": "./lib/main.dart",
|
||||
"type": "dart",
|
||||
"preLaunchTask": "AF: Clean + Rebuild All (Android)",
|
||||
"env": {
|
||||
"RUST_LOG": "trace"
|
||||
},
|
||||
"cwd": "${workspaceRoot}/appflowy_flutter"
|
||||
},
|
||||
{
|
||||
"name": "AF-desktop: Debug Rust",
|
||||
"type": "lldb",
|
||||
"request": "attach",
|
||||
"pid": "${command:pickMyProcess}"
|
||||
// To launch the application directly, use the following configuration:
|
||||
// "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,6 +245,51 @@
|
||||
"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.8.2"
|
||||
APPFLOWY_VERSION = "0.8.0"
|
||||
FLUTTER_DESKTOP_FEATURES = "dart"
|
||||
PRODUCT_NAME = "AppFlowy"
|
||||
MACOSX_DEPLOYMENT_TARGET = "11.0"
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'data_migration/data_migration_test_runner.dart'
|
||||
as data_migration_test_runner;
|
||||
import 'database/database_test_runner.dart' as database_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;
|
||||
@@ -29,7 +28,4 @@ Future<void> main() async {
|
||||
sidebar_move_page_test.main();
|
||||
sidebar_rename_untitled_test.main();
|
||||
sidebar_icon_test.main();
|
||||
|
||||
// database
|
||||
database_test_runner.main();
|
||||
}
|
||||
-80
@@ -1,80 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/core/config/kv.dart';
|
||||
import 'package:appflowy/core/config/kv_keys.dart';
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pbenum.dart';
|
||||
import 'package:appflowy_editor/appflowy_editor.dart'
|
||||
hide UploadImageMenu, ResizableImage;
|
||||
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:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
import '../../../shared/constants.dart';
|
||||
import '../../../shared/database_test_op.dart';
|
||||
import '../../../shared/mock/mock_file_picker.dart';
|
||||
import '../../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// copy link to block
|
||||
group('database image:', () {
|
||||
testWidgets('insert image', (tester) async {
|
||||
await tester.initializeAppFlowy(
|
||||
cloudType: AuthenticatorType.appflowyCloudSelfHost,
|
||||
);
|
||||
await tester.tapGoogleLoginInButton();
|
||||
await tester.expectToSeeHomePageWithGetStartedPage();
|
||||
|
||||
// open the first row detail page and upload an image
|
||||
await tester.createNewPageInSpace(
|
||||
spaceName: Constants.generalSpaceName,
|
||||
layout: ViewLayoutPB.Grid,
|
||||
pageName: 'database image',
|
||||
);
|
||||
await tester.openFirstRowDetailPage();
|
||||
|
||||
// insert an image block
|
||||
{
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.editor.showSlashMenu();
|
||||
await tester.editor.tapSlashMenuItemWithName(
|
||||
LocaleKeys.document_slashMenu_name_image.tr(),
|
||||
);
|
||||
}
|
||||
|
||||
// upload an image
|
||||
{
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final tempDirectory = await getTemporaryDirectory();
|
||||
final imagePath = p.join(tempDirectory.path, 'sample.jpeg');
|
||||
final file = File(imagePath)
|
||||
..writeAsBytesSync(image.buffer.asUint8List());
|
||||
|
||||
mockPickFilePaths(
|
||||
paths: [imagePath],
|
||||
);
|
||||
|
||||
await getIt<KeyValueStorage>().set(KVKeys.kCloudType, '0');
|
||||
await tester.tapButtonWithName(
|
||||
LocaleKeys.document_imageBlock_upload_placeholder.tr(),
|
||||
);
|
||||
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], isNotEmpty);
|
||||
|
||||
// remove the temp file
|
||||
file.deleteSync();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'database_image_test.dart' as database_image_test;
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
database_image_test.main();
|
||||
}
|
||||
+2
-2
@@ -45,7 +45,7 @@ void main() {
|
||||
);
|
||||
|
||||
final icons = find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.iconContent,
|
||||
);
|
||||
expect(icons, findsOneWidget);
|
||||
await tester.tapIcon(EmojiIconData.icon(firstIcon));
|
||||
@@ -54,7 +54,7 @@ void main() {
|
||||
final spaceIcon = find.descendant(
|
||||
of: spaceHeader,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.svgString,
|
||||
(w) => w is FlowySvg && w.svgString == firstIcon.iconContent,
|
||||
),
|
||||
);
|
||||
expect(spaceIcon, findsOneWidget);
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
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';
|
||||
|
||||
+1
-1
@@ -54,7 +54,7 @@ void main() {
|
||||
);
|
||||
|
||||
final shareValues = plainText!
|
||||
.replaceAll('${ShareConstants.defaultBaseWebDomain}/app/', '')
|
||||
.replaceAll('https://${ShareConstants.shareBaseUrl}/', '')
|
||||
.split('/');
|
||||
final workspaceId = shareValues[0];
|
||||
expect(workspaceId, isNotEmpty);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import 'package:appflowy/env/cloud_env.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_page.dart';
|
||||
import 'package:appflowy/shared/loading.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/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';
|
||||
@@ -72,16 +71,5 @@ 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
-9
@@ -120,10 +120,6 @@ void main() {
|
||||
widget is PublishedViewItem &&
|
||||
widget.publishInfoView.view.name == pageName,
|
||||
);
|
||||
if (pageItem.evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(pageItem, findsOneWidget);
|
||||
|
||||
// comment it out because it's not allowed to update the namespace in free plan
|
||||
@@ -253,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(2000);
|
||||
await tester.wait(1000);
|
||||
|
||||
// check if the page is published in sites page
|
||||
final pageItem = find.byWidgetPredicate(
|
||||
@@ -261,10 +257,6 @@ More actions for published page:
|
||||
widget is PublishedViewItem &&
|
||||
widget.publishInfoView.view.name == pageName,
|
||||
);
|
||||
if (pageItem.evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
expect(pageItem, findsOneWidget);
|
||||
|
||||
final copyLinkItem = find.text(LocaleKeys.shareAction_copyLink.tr());
|
||||
|
||||
+3
-99
@@ -1,15 +1,6 @@
|
||||
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';
|
||||
@@ -17,9 +8,7 @@ import 'package:integration_test/integration_test.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
setUpAll(() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
});
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('Folder Search', () {
|
||||
testWidgets('Search for views', (tester) async {
|
||||
@@ -56,94 +45,9 @@ 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,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,190 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_add_button.dart';
|
||||
import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
|
||||
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.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/shared/sidebar_folder.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/setting_entities.pbenum.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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('change icon', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
|
||||
const pageName = 'Database';
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
layout: ViewLayoutPB.Grid,
|
||||
name: pageName,
|
||||
);
|
||||
|
||||
/// create board
|
||||
final addButton = find.byType(AddDatabaseViewButton);
|
||||
await tester.tapButton(addButton);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Board.layoutName}',
|
||||
findRichText: true,
|
||||
),
|
||||
);
|
||||
|
||||
/// create calendar
|
||||
await tester.tapButton(addButton);
|
||||
await tester.tapButton(
|
||||
find.text(
|
||||
'${LocaleKeys.grid_createView.tr()} ${DatabaseLayoutPB.Calendar.layoutName}',
|
||||
findRichText: true,
|
||||
),
|
||||
);
|
||||
|
||||
final databaseTabBarItem = find.byType(DatabaseTabBarItem);
|
||||
expect(databaseTabBarItem, findsNWidgets(3));
|
||||
final gridItem = databaseTabBarItem.first,
|
||||
boardItem = databaseTabBarItem.at(1),
|
||||
calendarItem = databaseTabBarItem.last;
|
||||
|
||||
/// change the icon of grid
|
||||
/// the first tapping is to select specific item
|
||||
/// the second tapping is to show the menu
|
||||
await tester.tapButton(gridItem);
|
||||
await tester.tapButton(gridItem);
|
||||
|
||||
/// change icon
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final gridIcon = find.descendant(
|
||||
of: gridItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final gridIconWidget =
|
||||
gridIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final iconsData = IconsData.fromJson(jsonDecode(iconData.emoji));
|
||||
final gridIconsData =
|
||||
IconsData.fromJson(jsonDecode(gridIconWidget.emoji.emoji));
|
||||
expect(gridIconsData.iconName, iconsData.iconName);
|
||||
|
||||
/// change the icon of board
|
||||
await tester.tapButton(boardItem);
|
||||
await tester.tapButton(boardItem);
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final boardIcon = find.descendant(
|
||||
of: boardItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final boardIconWidget =
|
||||
boardIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final boardIconsData =
|
||||
IconsData.fromJson(jsonDecode(boardIconWidget.emoji.emoji));
|
||||
expect(boardIconsData.iconName, iconsData.iconName);
|
||||
|
||||
/// change the icon of calendar
|
||||
await tester.tapButton(calendarItem);
|
||||
await tester.tapButton(calendarItem);
|
||||
await tester
|
||||
.tapButton(find.text(LocaleKeys.disclosureAction_changeIcon.tr()));
|
||||
await tester.tapIcon(iconData, enableColor: false);
|
||||
final calendarIcon = find.descendant(
|
||||
of: calendarItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final calendarIconWidget =
|
||||
calendarIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final calendarIconsData =
|
||||
IconsData.fromJson(jsonDecode(calendarIconWidget.emoji.emoji));
|
||||
expect(calendarIconsData.iconName, iconsData.iconName);
|
||||
});
|
||||
|
||||
testWidgets('change database icon from sidebar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
final iconData = await tester.loadIcon();
|
||||
final icon = IconsData.fromJson(jsonDecode(iconData.emoji)), emoji = '😄';
|
||||
|
||||
const pageName = 'Database';
|
||||
await tester.createNewPageWithNameUnderParent(
|
||||
layout: ViewLayoutPB.Grid,
|
||||
name: pageName,
|
||||
);
|
||||
final viewItem = find.descendant(
|
||||
of: find.byType(SidebarFolder),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is ViewItem && w.view.name == pageName,
|
||||
),
|
||||
);
|
||||
|
||||
/// change icon to emoji
|
||||
await tester.tapButton(
|
||||
find.descendant(
|
||||
of: viewItem,
|
||||
matching: find.byType(FlowySvg),
|
||||
),
|
||||
);
|
||||
await tester.tapEmoji(emoji);
|
||||
final iconWidget = find.descendant(
|
||||
of: viewItem,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
expect(
|
||||
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
|
||||
emoji,
|
||||
);
|
||||
|
||||
/// the icon will not be displayed in database item
|
||||
Finder databaseIcon = find.descendant(
|
||||
of: find.byType(DatabaseTabBarItem),
|
||||
matching: find.byType(FlowySvg),
|
||||
);
|
||||
expect(
|
||||
(databaseIcon.evaluate().first.widget as FlowySvg).svg,
|
||||
FlowySvgs.icon_grid_s,
|
||||
);
|
||||
|
||||
/// change emoji to icon
|
||||
await tester.tapButton(iconWidget);
|
||||
await tester.tapIcon(iconData);
|
||||
expect(
|
||||
(iconWidget.evaluate().first.widget as RawEmojiIconWidget).emoji.emoji,
|
||||
iconData.emoji,
|
||||
);
|
||||
|
||||
databaseIcon = find.descendant(
|
||||
of: find.byType(DatabaseTabBarItem),
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
final databaseIconWidget =
|
||||
databaseIcon.evaluate().first.widget as RawEmojiIconWidget;
|
||||
final databaseIconsData =
|
||||
IconsData.fromJson(jsonDecode(databaseIconWidget.emoji.emoji));
|
||||
expect(icon.svgString, databaseIconsData.svgString);
|
||||
expect(icon.color, isNotEmpty);
|
||||
expect(icon.color, databaseIconsData.color);
|
||||
|
||||
/// the icon in database item should not show the color
|
||||
expect(databaseIconWidget.enableColor, false);
|
||||
});
|
||||
}
|
||||
-67
@@ -1,67 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
+315
-325
@@ -19,7 +19,7 @@ import '../../shared/util.dart';
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('copy and paste in document:', () {
|
||||
group('copy and paste in document', () {
|
||||
testWidgets('paste multiple lines at the first line', (tester) async {
|
||||
// mock the clipboard
|
||||
const lines = 3;
|
||||
@@ -172,314 +172,305 @@ void main() {
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste text on part of bullet list', (tester) async {
|
||||
const plainText = 'test';
|
||||
testWidgets('paste text on part of bullet list', (tester) async {
|
||||
const plainText = 'test';
|
||||
|
||||
await tester.pasteContent(
|
||||
plainText: plainText,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "bullet list"},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: [0], offset: 7),
|
||||
end: Position(path: [0], offset: 11),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||
expect(node?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('png', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(jpeg) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(gif) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.gif');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('gif', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'format the selected text to href when pasting url if available',
|
||||
(tester) async {
|
||||
const text = 'appflowy';
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(
|
||||
plainText: url,
|
||||
beforeTest: (editorState) async {
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': text,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
|
||||
testWidgets(
|
||||
'paste the image from clipboard when html and image are both available',
|
||||
(tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
image: ('png', bytes),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: bordeLine truncated
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
// hover on the link preview block
|
||||
// click the more button
|
||||
// and select convert to link
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(CustomLinkPreviewWidget),
|
||||
onHover: () async {
|
||||
final convertToLinkButton = find.byWidgetPredicate((widget) {
|
||||
return widget is MenuBlockButton &&
|
||||
widget.tooltip ==
|
||||
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
|
||||
});
|
||||
expect(convertToLinkButton, findsOneWidget);
|
||||
await tester.tap(convertToLinkButton);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final textNode = editorState.getNodeAtPath([0])!;
|
||||
expect(textNode.type, ParagraphBlockKeys.type);
|
||||
expect(textNode.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'ctrl/cmd+z to undo the auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'paste the nodes start with non-delta node',
|
||||
(tester) async {
|
||||
await tester.pasteContent((_) {});
|
||||
const text = 'Hello World';
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
await tester.pasteContent(
|
||||
plainText: plainText,
|
||||
beforeTest: (editorState) async {
|
||||
final transaction = editorState.transaction;
|
||||
// [image_block]
|
||||
// [paragraph_block]
|
||||
transaction.insertNodes([
|
||||
0,
|
||||
], [
|
||||
customImageNode(url: ''),
|
||||
paragraphNode(text: text),
|
||||
]);
|
||||
transaction.insertNodes(
|
||||
[0],
|
||||
[
|
||||
Node(
|
||||
type: BulletedListBlockKeys.type,
|
||||
attributes: {
|
||||
'delta': [
|
||||
{"insert": "bullet list"},
|
||||
],
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
// Set the selection to the second numbered list node (which has empty delta)
|
||||
transaction.afterSelection = Selection(
|
||||
start: Position(path: [0], offset: 7),
|
||||
end: Position(path: [0], offset: 11),
|
||||
);
|
||||
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// select all and copy
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
// put the cursor to the end of the paragraph block
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// paste the content
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect the image and the paragraph block are inserted below the cursor
|
||||
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0]);
|
||||
expect(node?.delta?.toPlainText(), 'bullet test');
|
||||
expect(node?.type, BulletedListBlockKeys.type);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('paste the url without protocol', (tester) async {
|
||||
// paste the image that from local file
|
||||
const plainText = '1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
testWidgets('paste image(png) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('png', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(jpeg) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('jpeg', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image(gif) from memory', (tester) async {
|
||||
final image = await rootBundle.load('assets/test/images/sample.gif');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(image: ('gif', bytes), (editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotNull);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'format the selected text to href when pasting url if available',
|
||||
(tester) async {
|
||||
const text = 'appflowy';
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(
|
||||
plainText: url,
|
||||
beforeTest: (editorState) async {
|
||||
await tester.ime.insertText(text);
|
||||
await tester.editor.updateSelection(
|
||||
Selection.single(
|
||||
path: [0],
|
||||
startOffset: 0,
|
||||
endOffset: text.length,
|
||||
),
|
||||
);
|
||||
},
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': text,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// https://github.com/AppFlowy-IO/AppFlowy/issues/3263
|
||||
testWidgets(
|
||||
'paste the image from clipboard when html and image are both available',
|
||||
(tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><img src="https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png" alt="image"/>''';
|
||||
final image = await rootBundle.load('assets/test/images/sample.png');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
await tester.pasteContent(
|
||||
html: html,
|
||||
image: ('png', bytes),
|
||||
(editorState) {
|
||||
expect(editorState.document.root.children.length, 1);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the image url', (tester) async {
|
||||
const plainText = 'http://example.com/1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
testWidgets('paste the html content contains section', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">AppFlowy</strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;">Hello World</strong></span></section>''';
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node1 = editorState.getNodeAtPath([0])!;
|
||||
final node2 = editorState.getNodeAtPath([1])!;
|
||||
expect(node1.type, ParagraphBlockKeys.type);
|
||||
expect(node2.type, ParagraphBlockKeys.type);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste image url without extension', (tester) async {
|
||||
const plainText =
|
||||
'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
|
||||
await tester.pasteContent(plainText: plainText, (editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
testWidgets('paste the html from google translation', (tester) async {
|
||||
const html =
|
||||
'''<meta charset='utf-8'><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">new force</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><span style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgb(0, 160, 113);"><strong style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; vertical-align: inherit;">Assessment focus: potential motivations, empathy</font></font></strong></span></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><br style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;"></section><section style="margin: 0px 8px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box; overflow-wrap: break-word !important; color: rgba(255, 255, 255, 0.6); font-family: system-ui, -apple-system, "system-ui", "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; font-size: 15px; font-style: normal; font-variant-ligatures: normal; font-variant-caps: normal; font-weight: 400; letter-spacing: 0.544px; orphans: 2; text-align: justify; text-indent: 0px; text-transform: none; widows: 2; word-spacing: 0px; -webkit-text-stroke-width: 0px; white-space: normal; background-color: rgb(25, 25, 25); text-decoration-thickness: initial; text-decoration-style: initial; text-decoration-color: initial;"><font style="margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-Line truncated
|
||||
await tester.pasteContent(html: html, (editorState) {
|
||||
expect(editorState.document.root.children.length, 8);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
testWidgets(
|
||||
'auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
// hover on the link preview block
|
||||
// click the more button
|
||||
// and select convert to link
|
||||
await tester.hoverOnWidget(
|
||||
find.byType(CustomLinkPreviewWidget),
|
||||
onHover: () async {
|
||||
final convertToLinkButton = find.byWidgetPredicate((widget) {
|
||||
return widget is MenuBlockButton &&
|
||||
widget.tooltip ==
|
||||
LocaleKeys.document_plugins_urlPreview_convertToLink.tr();
|
||||
});
|
||||
expect(convertToLinkButton, findsOneWidget);
|
||||
await tester.tap(convertToLinkButton);
|
||||
await tester.pumpAndSettle();
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final textNode = editorState.getNodeAtPath([0])!;
|
||||
expect(textNode.type, ParagraphBlockKeys.type);
|
||||
expect(textNode.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'ctrl/cmd+z to undo the auto convert url to link preview block',
|
||||
(tester) async {
|
||||
const url = 'https://appflowy.io';
|
||||
await tester.pasteContent(plainText: url, (editorState) async {
|
||||
// the second one is the paragraph node
|
||||
expect(editorState.document.root.children.length, 2);
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, LinkPreviewBlockKeys.type);
|
||||
expect(node.attributes[LinkPreviewBlockKeys.url], url);
|
||||
});
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyZ,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ParagraphBlockKeys.type);
|
||||
expect(node.delta!.toJson(), [
|
||||
{
|
||||
'insert': url,
|
||||
'attributes': {'href': url},
|
||||
}
|
||||
]);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets(
|
||||
'paste the nodes start with non-delta node',
|
||||
(tester) async {
|
||||
await tester.pasteContent((_) {});
|
||||
const text = 'Hello World';
|
||||
final editorState = tester.editor.getCurrentEditorState();
|
||||
final transaction = editorState.transaction;
|
||||
// [image_block]
|
||||
// [paragraph_block]
|
||||
transaction.insertNodes([
|
||||
0,
|
||||
], [
|
||||
customImageNode(url: ''),
|
||||
paragraphNode(text: text),
|
||||
]);
|
||||
await editorState.apply(transaction);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
// select all and copy
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyA,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyC,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
|
||||
// put the cursor to the end of the paragraph block
|
||||
await tester.editor.tapLineOfEditorAt(0);
|
||||
|
||||
// paste the content
|
||||
await tester.simulateKeyEvent(
|
||||
LogicalKeyboardKey.keyV,
|
||||
isControlPressed:
|
||||
UniversalPlatform.isLinux || UniversalPlatform.isWindows,
|
||||
isMetaPressed: UniversalPlatform.isMacOS,
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// expect the image and the paragraph block are inserted below the cursor
|
||||
expect(editorState.getNodeAtPath([0])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([1])!.type, ParagraphBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([2])!.type, CustomImageBlockKeys.type);
|
||||
expect(editorState.getNodeAtPath([3])!.type, ParagraphBlockKeys.type);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('paste the url without protocol', (tester) async {
|
||||
// paste the image that from local file
|
||||
const plainText = '1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
testWidgets('paste the image url', (tester) async {
|
||||
const plainText = 'https://appflowy.io/1.jpg';
|
||||
final image = await rootBundle.load('assets/test/images/sample.jpeg');
|
||||
final bytes = image.buffer.asUint8List();
|
||||
await tester.pasteContent(plainText: plainText, image: ('jpeg', bytes),
|
||||
(editorState) {
|
||||
final node = editorState.getNodeAtPath([0])!;
|
||||
expect(node.type, ImageBlockKeys.type);
|
||||
expect(node.attributes[ImageBlockKeys.url], isNotEmpty);
|
||||
});
|
||||
});
|
||||
|
||||
const testMarkdownText = '''
|
||||
# I'm h1
|
||||
## I'm h2
|
||||
### I'm h3
|
||||
@@ -487,35 +478,34 @@ void main() {
|
||||
##### 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', (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);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
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);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -533,7 +523,7 @@ extension on WidgetTester {
|
||||
await tapAnonymousSignInButton();
|
||||
|
||||
// create a new document
|
||||
await createNewPageWithNameUnderParent();
|
||||
await createNewPageWithNameUnderParent(name: 'Test Document');
|
||||
// tap the editor
|
||||
await tapButton(find.byType(AppFlowyEditor));
|
||||
|
||||
@@ -556,7 +546,7 @@ extension on WidgetTester {
|
||||
isShiftPressed: pasteAsPlain,
|
||||
isMetaPressed: Platform.isMacOS,
|
||||
);
|
||||
await pumpAndSettle(const Duration(milliseconds: 1000));
|
||||
await pumpAndSettle();
|
||||
|
||||
test(editor.getCurrentEditorState());
|
||||
}
|
||||
|
||||
-106
@@ -1,10 +1,4 @@
|
||||
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';
|
||||
|
||||
@@ -37,104 +31,4 @@ 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(),
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
+57
-5
@@ -58,6 +58,11 @@ void main() {
|
||||
|
||||
await tester.insertSubPageFromSlashMenu();
|
||||
|
||||
await tester.expandOrCollapsePage(
|
||||
pageName: 'SubPageBlock',
|
||||
layout: ViewLayoutPB.Document,
|
||||
);
|
||||
|
||||
expect(
|
||||
find.text(LocaleKeys.menuAppHeader_defaultNewPageName.tr()),
|
||||
findsNWidgets(3),
|
||||
@@ -72,6 +77,12 @@ 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));
|
||||
|
||||
@@ -90,6 +101,11 @@ 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));
|
||||
|
||||
@@ -138,6 +154,11 @@ 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));
|
||||
|
||||
@@ -191,6 +212,11 @@ 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));
|
||||
|
||||
@@ -227,6 +253,11 @@ 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));
|
||||
|
||||
@@ -272,6 +303,11 @@ 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));
|
||||
|
||||
@@ -310,6 +346,11 @@ 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));
|
||||
|
||||
@@ -353,6 +394,11 @@ 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);
|
||||
@@ -375,6 +421,12 @@ 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));
|
||||
|
||||
@@ -395,6 +447,11 @@ 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]);
|
||||
@@ -475,11 +532,6 @@ void main() {
|
||||
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;
|
||||
|
||||
-61
@@ -1,23 +1,15 @@
|
||||
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() {
|
||||
@@ -68,59 +60,6 @@ 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();
|
||||
|
||||
+92
@@ -7,16 +7,19 @@ 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';
|
||||
@@ -26,6 +29,58 @@ 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();
|
||||
@@ -76,6 +131,43 @@ 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 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();
|
||||
|
||||
+4
-6
@@ -35,12 +35,10 @@ void main() {
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.hoverOnWidget(
|
||||
find
|
||||
.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
)
|
||||
.first,
|
||||
find.descendant(
|
||||
of: find.byType(ShortcutSettingTile),
|
||||
matching: find.text(backspaceCmd),
|
||||
),
|
||||
onHover: () async {
|
||||
await tester.tap(find.byFlowySvg(FlowySvgs.edit_s));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
+7
-19
@@ -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,31 +45,24 @@ 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://self-hosted.appflowy.cloud';
|
||||
const serverUrl = 'https://test.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(DesktopToast));
|
||||
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// open settings page to check the result
|
||||
@@ -86,11 +79,6 @@ void main() {
|
||||
find.text(serverUrl),
|
||||
findsOneWidget,
|
||||
);
|
||||
// check the web url
|
||||
expect(
|
||||
find.text(webUrl),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
// reset to appflowy cloud
|
||||
await tester.tapButton(
|
||||
@@ -102,7 +90,7 @@ void main() {
|
||||
);
|
||||
|
||||
// wait the app to restart, and the tooltip to disappear
|
||||
await tester.pumpUntilNotFound(find.byType(DesktopToast));
|
||||
await tester.pumpUntilNotFound(find.byType(BuiltInToastBuilder));
|
||||
await tester.pumpAndSettle(const Duration(milliseconds: 250));
|
||||
|
||||
// check the server type
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
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';
|
||||
|
||||
@@ -48,82 +44,5 @@ 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,58 +196,5 @@ 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']);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:io';
|
||||
|
||||
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';
|
||||
@@ -7,11 +5,8 @@ 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/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 '../../shared/base.dart';
|
||||
import '../../shared/common_operations.dart';
|
||||
@@ -214,49 +209,4 @@ void main() {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets('Update page custom icon in title bar', (tester) async {
|
||||
await tester.initializeAppFlowy();
|
||||
await tester.tapAnonymousSignInButton();
|
||||
|
||||
/// prepare local 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());
|
||||
final iconData = EmojiIconData.custom(imageFile.path);
|
||||
|
||||
// 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
@@ -1,56 +0,0 @@
|
||||
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,7 +2,6 @@ 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;
|
||||
|
||||
@@ -15,5 +14,4 @@ void main() {
|
||||
sidebar_favorite_test.main();
|
||||
sidebar_icon_test.main();
|
||||
sidebar_view_item_test.main();
|
||||
sidebar_recent_icon_test.main();
|
||||
}
|
||||
@@ -1,15 +1,11 @@
|
||||
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';
|
||||
@@ -289,39 +285,6 @@ 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import 'package:integration_test/integration_test.dart';
|
||||
|
||||
import 'desktop/database/database_icon_test.dart' as database_icon_test;
|
||||
import 'desktop/first_test/first_test.dart' as first_test;
|
||||
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
|
||||
import 'desktop/uncategorized/code_block_language_selector_test.dart'
|
||||
as code_language_selector;
|
||||
import 'desktop/uncategorized/tabs_test.dart' as tabs_test;
|
||||
import 'desktop/first_test/first_test.dart' as first_test;
|
||||
|
||||
Future<void> main() async {
|
||||
await runIntegration9OnDesktop();
|
||||
@@ -16,5 +15,4 @@ Future<void> runIntegration9OnDesktop() async {
|
||||
first_test.main();
|
||||
tabs_test.main();
|
||||
code_language_selector.main();
|
||||
database_icon_test.main();
|
||||
}
|
||||
@@ -4,7 +4,6 @@ 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();
|
||||
@@ -14,5 +13,4 @@ void main() {
|
||||
page_style_test.main();
|
||||
plus_menu_test.main();
|
||||
simple_table_test.main();
|
||||
toolbar_test.main();
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/_page_style_icon.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.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 '../../shared/emoji.dart';
|
||||
import '../../shared/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
group('document title:', () {
|
||||
testWidgets('update page custom icon in title bar', (tester) async {
|
||||
await tester.launchInAnonymousMode();
|
||||
|
||||
/// prepare local 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());
|
||||
final iconData = EmojiIconData.custom(imageFile.path);
|
||||
|
||||
/// create an empty page
|
||||
await tester
|
||||
.tapButton(find.byKey(BottomNavigationBarItemType.add.valueKey));
|
||||
|
||||
/// show Page style page
|
||||
await tester.tapButton(find.byType(MobileViewPageLayoutButton));
|
||||
final pageStyleIcon = find.byType(PageStyleIcon);
|
||||
final iconInPageStyleIcon = find.descendant(
|
||||
of: pageStyleIcon,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
);
|
||||
expect(iconInPageStyleIcon, findsNothing);
|
||||
|
||||
/// show icon picker
|
||||
await tester.tapButton(pageStyleIcon);
|
||||
|
||||
/// upload custom icon
|
||||
await tester.pickImage(iconData);
|
||||
|
||||
/// check result
|
||||
final documentPage = find.byType(MobileDocumentScreen);
|
||||
final rawEmojiIconWidget = find
|
||||
.descendant(
|
||||
of: documentPage,
|
||||
matching: find.byType(RawEmojiIconWidget),
|
||||
)
|
||||
.evaluate()
|
||||
.first
|
||||
.widget as RawEmojiIconWidget;
|
||||
final iconDataInWidget = rawEmojiIconWidget.emoji;
|
||||
expect(iconDataInWidget.type, FlowyIconType.custom);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -494,61 +494,5 @@ void main() {
|
||||
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));
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1,117 +0,0 @@
|
||||
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
@@ -1,81 +0,0 @@
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/setting/settings_popup_menu.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.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/util.dart';
|
||||
|
||||
void main() {
|
||||
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
testWidgets('test for change scale factor', (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 [Font Scale Factor]
|
||||
await tester.tapButton(
|
||||
find.text(LocaleKeys.settings_appearance_fontScaleFactor.tr()),
|
||||
);
|
||||
|
||||
/// drag slider
|
||||
final slider = find.descendant(
|
||||
of: find.byType(FontSizeStepper),
|
||||
matching: find.byType(Slider),
|
||||
);
|
||||
await tester.slideToValue(slider, 0.8);
|
||||
expect(appflowyScaleFactor, 0.8);
|
||||
|
||||
await tester.slideToValue(slider, 0.9);
|
||||
expect(appflowyScaleFactor, 0.9);
|
||||
|
||||
await tester.slideToValue(slider, 1.0);
|
||||
expect(appflowyScaleFactor, 1.0);
|
||||
|
||||
await tester.slideToValue(slider, 1.1);
|
||||
expect(appflowyScaleFactor, 1.1);
|
||||
|
||||
await tester.slideToValue(slider, 1.2);
|
||||
expect(appflowyScaleFactor, 1.2);
|
||||
});
|
||||
}
|
||||
@@ -3,8 +3,6 @@ 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 {
|
||||
@@ -19,5 +17,4 @@ Future<void> runIntegration1OnMobile() async {
|
||||
anonymous_sign_in_test.main();
|
||||
create_new_page_test.main();
|
||||
document_test_runner.main();
|
||||
default_text_direction_test.main();
|
||||
}
|
||||
@@ -13,7 +13,6 @@ import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||
import 'package:flowy_infra/uuid.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
@@ -236,25 +235,6 @@ extension AppFlowyTestBase on WidgetTester {
|
||||
Future<void> wait(int milliseconds) async {
|
||||
await pumpAndSettle(Duration(milliseconds: milliseconds));
|
||||
}
|
||||
|
||||
Future<void> slideToValue(
|
||||
Finder slider,
|
||||
double value, {
|
||||
double paddingOffset = 24.0,
|
||||
}) async {
|
||||
final sliderWidget = slider.evaluate().first.widget as Slider;
|
||||
final range = sliderWidget.max - sliderWidget.min;
|
||||
final initialRate = (value - sliderWidget.min) / range;
|
||||
final totalWidth = getSize(slider).width - (2 * paddingOffset);
|
||||
final zeroPoint = getTopLeft(slider) +
|
||||
Offset(
|
||||
paddingOffset + initialRate * totalWidth,
|
||||
getSize(slider).height / 2,
|
||||
);
|
||||
final calculatedOffset = value * (totalWidth / 100);
|
||||
await dragFrom(zeroPoint, Offset(calculatedOffset, 0));
|
||||
await pumpAndSettle();
|
||||
}
|
||||
}
|
||||
|
||||
extension AppFlowyFinderTestBase on CommonFinders {
|
||||
|
||||
@@ -19,7 +19,6 @@ 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';
|
||||
@@ -601,23 +600,6 @@ 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(
|
||||
@@ -629,7 +611,7 @@ extension CommonOperations on WidgetTester {
|
||||
// update the page icon in the sidebar
|
||||
Future<void> updatePageIconInSidebarByName({
|
||||
required String name,
|
||||
String? parentName,
|
||||
required String parentName,
|
||||
required ViewLayoutPB layout,
|
||||
required EmojiIconData icon,
|
||||
}) async {
|
||||
@@ -671,8 +653,6 @@ extension CommonOperations on WidgetTester {
|
||||
await tapEmoji(icon.emoji);
|
||||
} else if (icon.type == FlowyIconType.icon) {
|
||||
await tapIcon(icon);
|
||||
} else if (icon.type == FlowyIconType.custom) {
|
||||
await pickImage(icon);
|
||||
}
|
||||
await pumpAndSettle();
|
||||
}
|
||||
@@ -930,6 +910,7 @@ extension CommonOperations on WidgetTester {
|
||||
return EmojiIconData.icon(
|
||||
IconsData(
|
||||
firstGroup.name,
|
||||
firstIcon.content,
|
||||
firstIcon.name,
|
||||
builtInSpaceColors.first,
|
||||
),
|
||||
|
||||
@@ -3,12 +3,8 @@ 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/icon_uploader.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:cross_file/cross_file.dart';
|
||||
import 'package:desktop_drop/desktop_drop.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/primary_rounded_button.dart';
|
||||
import 'package:flowy_svg/flowy_svg.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_emoji_mart/flutter_emoji_mart.dart';
|
||||
@@ -25,7 +21,7 @@ extension EmojiTestExtension on WidgetTester {
|
||||
await tapButton(emojiWidget);
|
||||
}
|
||||
|
||||
Future<void> tapIcon(EmojiIconData icon, {bool enableColor = true}) async {
|
||||
Future<void> tapIcon(EmojiIconData icon) async {
|
||||
final iconsData = IconsData.fromJson(jsonDecode(icon.emoji));
|
||||
final pickTab = find.byType(PickerTab);
|
||||
expect(pickTab, findsOneWidget);
|
||||
@@ -39,69 +35,33 @@ extension EmojiTestExtension on WidgetTester {
|
||||
final selectedSvg = find.descendant(
|
||||
of: find.byType(FlowyIconPicker),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
|
||||
),
|
||||
);
|
||||
expect(find.byType(IconColorPicker), findsNothing);
|
||||
|
||||
await tapButton(selectedSvg.first);
|
||||
if (enableColor) {
|
||||
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;
|
||||
}
|
||||
/// test for tapping down, it should not display the ColorPicker unless tapping up
|
||||
await tapDown(selectedSvg);
|
||||
expect(find.byType(IconColorPicker), findsNothing);
|
||||
|
||||
await tapButton(selectedSvg);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> pickImage(EmojiIconData icon) async {
|
||||
final pickTab = find.byType(PickerTab);
|
||||
expect(pickTab, findsOneWidget);
|
||||
await pumpAndSettle();
|
||||
|
||||
/// switch to custom tab
|
||||
final iconTab = find.descendant(
|
||||
of: pickTab,
|
||||
matching: find.text(PickerTabType.custom.tr),
|
||||
}
|
||||
return false;
|
||||
}),
|
||||
);
|
||||
expect(iconTab, findsOneWidget);
|
||||
await tapButton(iconTab);
|
||||
|
||||
/// mock for dragging image
|
||||
final dropTarget = find.descendant(
|
||||
of: find.byType(IconUploader),
|
||||
matching: find.byType(DropTarget),
|
||||
);
|
||||
expect(dropTarget, findsOneWidget);
|
||||
final dropTargetWidget = dropTarget.evaluate().first.widget as DropTarget;
|
||||
dropTargetWidget.onDragDone?.call(
|
||||
DropDoneDetails(
|
||||
files: [XFile(icon.emoji)],
|
||||
localPosition: Offset.zero,
|
||||
globalPosition: Offset.zero,
|
||||
),
|
||||
);
|
||||
await pumpAndSettle(const Duration(seconds: 3));
|
||||
|
||||
/// confirm to upload
|
||||
final confirmButton = find.descendant(
|
||||
of: find.byType(IconUploader),
|
||||
matching: find.byType(PrimaryRoundedButton),
|
||||
);
|
||||
await tapButton(confirmButton);
|
||||
await tapButton(selectedColor);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,6 @@ 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/appflowy_network_image.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';
|
||||
@@ -22,7 +21,6 @@ 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:string_validator/string_validator.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'util.dart';
|
||||
@@ -247,24 +245,10 @@ extension Expectation on WidgetTester {
|
||||
final icon = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
|
||||
),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
} else if (type == FlowyIconType.custom) {
|
||||
if (isURL(data.emoji)) {
|
||||
final image = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.byType(FlowyNetworkImage),
|
||||
);
|
||||
expect(image, findsOneWidget);
|
||||
} else {
|
||||
final image = find.descendant(
|
||||
of: pageName,
|
||||
matching: find.byType(Image),
|
||||
);
|
||||
expect(image, findsOneWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,24 +269,10 @@ extension Expectation on WidgetTester {
|
||||
final icon = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.byWidgetPredicate(
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.svgString,
|
||||
(w) => w is FlowySvg && w.svgString == iconsData.iconContent,
|
||||
),
|
||||
);
|
||||
expect(icon, findsOneWidget);
|
||||
} else if (type == FlowyIconType.custom) {
|
||||
if (isURL(data.emoji)) {
|
||||
final image = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.byType(FlowyNetworkImage),
|
||||
);
|
||||
expect(image, findsOneWidget);
|
||||
} else {
|
||||
final image = find.descendant(
|
||||
of: find.byType(ViewTitleBar),
|
||||
matching: find.byType(Image),
|
||||
);
|
||||
expect(image, findsOneWidget);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:appflowy/ai/service/error.dart';
|
||||
import 'package:appflowy/ai/service/openai_client.dart';
|
||||
import 'package:appflowy/ai/service/text_completion.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/error.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/openai_client.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/text_completion.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:mocktail/mocktail.dart';
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
export 'widgets/loading_indicator.dart';
|
||||
export 'widgets/prompt_input/action_buttons.dart';
|
||||
export 'widgets/prompt_input/desktop_input_text_field.dart';
|
||||
export 'widgets/prompt_input/file_attachment_list.dart';
|
||||
export 'widgets/prompt_input/layout_define.dart';
|
||||
export 'widgets/prompt_input/mention_page_bottom_sheet.dart';
|
||||
export 'widgets/prompt_input/mention_page_menu.dart';
|
||||
export 'widgets/prompt_input/mentioned_page_text_span.dart';
|
||||
export 'widgets/prompt_input/predefined_format_buttons.dart';
|
||||
export 'widgets/prompt_input/select_sources_bottom_sheet.dart';
|
||||
export 'widgets/prompt_input/select_sources_menu.dart';
|
||||
export 'widgets/prompt_input/send_button.dart';
|
||||
@@ -1,53 +0,0 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_input_control_cubit.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/layout_define.dart';
|
||||
import 'package:extended_text_field/extended_text_field.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'mentioned_page_text_span.dart';
|
||||
|
||||
class PromptInputTextField extends StatelessWidget {
|
||||
const PromptInputTextField({
|
||||
super.key,
|
||||
required this.cubit,
|
||||
required this.textController,
|
||||
required this.textFieldFocusNode,
|
||||
required this.contentPadding,
|
||||
this.hintText = "",
|
||||
});
|
||||
|
||||
final ChatInputControlCubit cubit;
|
||||
final TextEditingController textController;
|
||||
final FocusNode textFieldFocusNode;
|
||||
final EdgeInsetsGeometry contentPadding;
|
||||
final String hintText;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ExtendedTextField(
|
||||
controller: textController,
|
||||
focusNode: textFieldFocusNode,
|
||||
decoration: InputDecoration(
|
||||
border: InputBorder.none,
|
||||
enabledBorder: InputBorder.none,
|
||||
focusedBorder: InputBorder.none,
|
||||
contentPadding: contentPadding,
|
||||
hintText: hintText,
|
||||
hintStyle: AIChatUILayout.inputHintTextStyle(context),
|
||||
isCollapsed: true,
|
||||
isDense: true,
|
||||
),
|
||||
keyboardType: TextInputType.multiline,
|
||||
textCapitalization: TextCapitalization.sentences,
|
||||
minLines: 1,
|
||||
maxLines: null,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
specialTextSpanBuilder: PromptInputTextSpanBuilder(
|
||||
inputControlCubit: cubit,
|
||||
specialTextStyle: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import 'package:flutter/widgets.dart';
|
||||
|
||||
class DesktopAIPromptSizes {
|
||||
const DesktopAIPromptSizes._();
|
||||
|
||||
static const attachedFilesBarPadding =
|
||||
EdgeInsets.only(left: 8.0, top: 8.0, right: 8.0);
|
||||
static const attachedFilesPreviewHeight = 48.0;
|
||||
static const attachedFilesPreviewSpacing = 12.0;
|
||||
|
||||
static const predefinedFormatButtonHeight = 28.0;
|
||||
static const predefinedFormatIconHeight = 16.0;
|
||||
|
||||
static const textFieldMinHeight = 36.0;
|
||||
static const textFieldContentPadding =
|
||||
EdgeInsetsDirectional.fromSTEB(14.0, 8.0, 14.0, 8.0);
|
||||
|
||||
static const actionBarButtonSize = 28.0;
|
||||
static const actionBarIconSize = 16.0;
|
||||
static const actionBarSendButtonSize = 32.0;
|
||||
static const actionBarSendButtonIconSize = 24.0;
|
||||
}
|
||||
|
||||
class MobileAIPromptSizes {
|
||||
const MobileAIPromptSizes._();
|
||||
|
||||
static const attachedFilesBarHeight = 68.0;
|
||||
static const attachedFilesBarPadding =
|
||||
EdgeInsets.only(top: 8.0, left: 8.0, right: 8.0, bottom: 4.0);
|
||||
static const attachedFilesPreviewHeight = 56.0;
|
||||
static const attachedFilesPreviewSpacing = 8.0;
|
||||
|
||||
static const predefinedFormatButtonHeight = 32.0;
|
||||
static const predefinedFormatIconHeight = 20.0;
|
||||
|
||||
static const textFieldMinHeight = 32.0;
|
||||
static const textFieldContentPadding =
|
||||
EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0);
|
||||
|
||||
static const mentionIconSize = 20.0;
|
||||
static const sendButtonSize = 32.0;
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
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 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'layout_define.dart';
|
||||
|
||||
class PromptInputDesktopToggleFormatButton extends StatelessWidget {
|
||||
const PromptInputDesktopToggleFormatButton({
|
||||
super.key,
|
||||
required this.showFormatBar,
|
||||
required this.predefinedFormat,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
final bool showFormatBar;
|
||||
final PredefinedFormat predefinedFormat;
|
||||
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.spacing,
|
||||
required this.onSelectPredefinedFormat,
|
||||
});
|
||||
|
||||
final PredefinedFormat? predefinedFormat;
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
double get _buttonSize {
|
||||
return UniversalPlatform.isMobile
|
||||
? MobileAIPromptSizes.predefinedFormatButtonHeight
|
||||
: DesktopAIPromptSizes.predefinedFormatButtonHeight;
|
||||
}
|
||||
|
||||
double get _iconSize {
|
||||
return UniversalPlatform.isMobile
|
||||
? MobileAIPromptSizes.predefinedFormatIconHeight
|
||||
: DesktopAIPromptSizes.predefinedFormatIconHeight;
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:universal_platform/universal_platform.dart';
|
||||
|
||||
import 'layout_define.dart';
|
||||
|
||||
enum SendButtonState { enabled, streaming, disabled }
|
||||
|
||||
class PromptInputSendButton extends StatelessWidget {
|
||||
const PromptInputSendButton({
|
||||
super.key,
|
||||
required this.state,
|
||||
required this.onSendPressed,
|
||||
required this.onStopStreaming,
|
||||
});
|
||||
|
||||
final SendButtonState state;
|
||||
final VoidCallback onSendPressed;
|
||||
final VoidCallback onStopStreaming;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
width: _buttonSize,
|
||||
icon: switch (state) {
|
||||
SendButtonState.enabled => FlowySvg(
|
||||
FlowySvgs.ai_send_filled_s,
|
||||
size: Size.square(_iconSize),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
SendButtonState.disabled => FlowySvg(
|
||||
FlowySvgs.ai_send_filled_s,
|
||||
size: Size.square(_iconSize),
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
SendButtonState.streaming => FlowySvg(
|
||||
FlowySvgs.ai_stop_filled_s,
|
||||
size: Size.square(_iconSize),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
},
|
||||
onPressed: () {
|
||||
switch (state) {
|
||||
case SendButtonState.enabled:
|
||||
onSendPressed();
|
||||
break;
|
||||
case SendButtonState.streaming:
|
||||
onStopStreaming();
|
||||
break;
|
||||
case SendButtonState.disabled:
|
||||
break;
|
||||
}
|
||||
},
|
||||
hoverColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
|
||||
double get _buttonSize {
|
||||
return UniversalPlatform.isMobile
|
||||
? MobileAIPromptSizes.sendButtonSize
|
||||
: DesktopAIPromptSizes.actionBarSendButtonSize;
|
||||
}
|
||||
|
||||
double get _iconSize {
|
||||
return UniversalPlatform.isMobile
|
||||
? MobileAIPromptSizes.sendButtonSize
|
||||
: DesktopAIPromptSizes.actionBarSendButtonIconSize;
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,6 @@ 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.
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// 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()
|
||||
@@ -42,7 +40,6 @@ 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) =>
|
||||
@@ -53,13 +50,6 @@ 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() {
|
||||
@@ -68,7 +58,6 @@ class AppFlowyCloudConfiguration {
|
||||
ws_base_url: '',
|
||||
gotrue_url: '',
|
||||
enable_sync_trace: false,
|
||||
base_web_domain: ShareConstants.defaultBaseWebDomain,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+1
-28
@@ -2,7 +2,6 @@ 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';
|
||||
|
||||
@@ -156,13 +155,6 @@ 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);
|
||||
@@ -221,7 +213,6 @@ 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(
|
||||
@@ -242,19 +233,16 @@ 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
|
||||
// in the development environment.
|
||||
// If you modify following code, please update the corresponding documentation in the appflowy billing.
|
||||
if (authenticatorType == AuthenticatorType.appflowyCloudDevelop) {
|
||||
return AppFlowyCloudConfiguration(
|
||||
base_url: "$baseUrl:8000",
|
||||
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(
|
||||
@@ -262,9 +250,6 @@ 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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -273,16 +258,10 @@ 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,
|
||||
baseShareDomain,
|
||||
);
|
||||
return await configurationFromUri(uri, baseURL, authenticatorType);
|
||||
} catch (e) {
|
||||
Log.error("Failed to parse AppFlowy Cloud URL: $e");
|
||||
return AppFlowyCloudConfiguration.defaultConfig();
|
||||
@@ -295,12 +274,6 @@ 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,6 +1,5 @@
|
||||
// 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';
|
||||
@@ -44,11 +43,4 @@ 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;
|
||||
}
|
||||
@@ -21,7 +21,6 @@ extension MobileRouter on BuildContext {
|
||||
bool showMoreButton = true,
|
||||
String? fixedTitle,
|
||||
String? blockId,
|
||||
List<String>? tabs,
|
||||
}) async {
|
||||
// set the current view before pushing the new view
|
||||
getIt<MenuSharedState>().latestOpenView = view;
|
||||
@@ -38,9 +37,6 @@ extension MobileRouter on BuildContext {
|
||||
queryParameters[MobileDocumentScreen.viewBlockId] = blockId;
|
||||
}
|
||||
}
|
||||
if (tabs != null) {
|
||||
queryParameters[MobileDocumentScreen.viewSelectTabs] = tabs.join('-');
|
||||
}
|
||||
|
||||
final uri = Uri(
|
||||
path: view.routeName,
|
||||
|
||||
@@ -10,7 +10,6 @@ import 'package:appflowy/plugins/document/presentation/document_collaborators.da
|
||||
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/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
|
||||
@@ -35,7 +34,6 @@ class MobileViewPage extends StatefulWidget {
|
||||
this.fixedTitle,
|
||||
this.showMoreButton = true,
|
||||
this.blockId,
|
||||
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
|
||||
});
|
||||
|
||||
/// view id
|
||||
@@ -45,7 +43,6 @@ class MobileViewPage extends StatefulWidget {
|
||||
final Map<String, dynamic>? arguments;
|
||||
final bool showMoreButton;
|
||||
final String? blockId;
|
||||
final List<PickerTabType> tabs;
|
||||
|
||||
// only used in row page
|
||||
final String? fixedTitle;
|
||||
@@ -196,7 +193,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
data: {
|
||||
MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,
|
||||
MobileDocumentScreen.viewBlockId: widget.blockId,
|
||||
MobileDocumentScreen.viewSelectTabs: widget.tabs,
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -246,7 +242,6 @@ class _MobileViewPageState extends State<MobileViewPage> {
|
||||
view: view,
|
||||
isImmersiveMode: isImmersiveMode,
|
||||
appBarOpacity: _appBarOpacity,
|
||||
tabs: widget.tabs,
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_notification.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/page_style/page_style_bottom_sheet.dart';
|
||||
import 'package:appflowy/plugins/shared/share/share_bloc.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
@@ -124,11 +123,9 @@ class MobileViewPageLayoutButton extends StatelessWidget {
|
||||
required this.view,
|
||||
required this.isImmersiveMode,
|
||||
required this.appBarOpacity,
|
||||
required this.tabs,
|
||||
});
|
||||
|
||||
final ViewPB view;
|
||||
final List<PickerTabType> tabs;
|
||||
final bool isImmersiveMode;
|
||||
final ValueListenable appBarOpacity;
|
||||
|
||||
@@ -159,7 +156,6 @@ class MobileViewPageLayoutButton extends StatelessWidget {
|
||||
],
|
||||
child: PageStyleBottomSheet(
|
||||
view: context.read<ViewBloc>().state.view,
|
||||
tabs: tabs,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
+6
-1
@@ -190,6 +190,7 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
MobileViewBottomSheetBodyAction.unpublish,
|
||||
),
|
||||
),
|
||||
_divider(),
|
||||
];
|
||||
} else {
|
||||
return [
|
||||
@@ -200,9 +201,13 @@ class MobileViewBottomSheetBody extends StatelessWidget {
|
||||
MobileViewBottomSheetBodyAction.publish,
|
||||
),
|
||||
),
|
||||
_divider(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Widget _divider() => const MobileQuickActionDivider();
|
||||
Widget _divider() => const Divider(
|
||||
height: 8.5,
|
||||
thickness: 0.5,
|
||||
);
|
||||
}
|
||||
+6
-2
@@ -7,6 +7,7 @@ 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';
|
||||
@@ -112,8 +113,12 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
context,
|
||||
showDragHandle: true,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
builder: (_) => Column(
|
||||
builder: (_) => SeparatedColumn(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
separatorBuilder: () => const Divider(
|
||||
height: 8.5,
|
||||
thickness: 0.5,
|
||||
),
|
||||
children: [
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.board_column_renameColumn.tr(),
|
||||
@@ -127,7 +132,6 @@ class _GroupCardHeaderState extends State<GroupCardHeader> {
|
||||
context.pop();
|
||||
},
|
||||
),
|
||||
const MobileQuickActionDivider(),
|
||||
MobileQuickActionButton(
|
||||
text: LocaleKeys.board_column_hideColumn.tr(),
|
||||
icon: FlowySvgs.hide_s,
|
||||
|
||||
+3
-2
@@ -149,7 +149,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.duplicate_s,
|
||||
text: LocaleKeys.button_duplicate.tr(),
|
||||
),
|
||||
const MobileQuickActionDivider(),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
MobileQuickActionButton(
|
||||
onTap: () => showMobileBottomSheet(
|
||||
context,
|
||||
@@ -201,7 +201,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.add_cover_s,
|
||||
text: 'Add cover',
|
||||
),
|
||||
const MobileQuickActionDivider(),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
MobileQuickActionButton(
|
||||
onTap: () => _performAction(viewId, _bloc.state.currentRowId, true),
|
||||
text: LocaleKeys.button_delete.tr(),
|
||||
@@ -209,6 +209,7 @@ class _MobileRowDetailPageState extends State<MobileRowDetailPage> {
|
||||
icon: FlowySvgs.trash_s,
|
||||
iconColor: Theme.of(context).colorScheme.error,
|
||||
),
|
||||
const Divider(height: 8.5, thickness: 0.5),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
-2
@@ -8,7 +8,6 @@ import 'package:appflowy/plugins/database/application/cell/cell_controller.dart'
|
||||
import 'package:appflowy/plugins/database/application/cell/cell_controller_builder.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/grid/presentation/layout/sizes.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/application/view/prelude.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
|
||||
import 'package:appflowy_backend/log.dart';
|
||||
@@ -117,7 +116,6 @@ class _OpenRowPageButtonState extends State<OpenRowPageButton> {
|
||||
addInRecent: false,
|
||||
showMoreButton: false,
|
||||
fixedTitle: fieldName,
|
||||
tabs: [PickerTabType.emoji.name],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-14
@@ -5,8 +5,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.dart';
|
||||
import 'package:appflowy/plugins/database/application/tab_bar_bloc.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_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
|
||||
@@ -165,20 +163,9 @@ class MobileDatabaseViewListButton extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildViewIconButton(BuildContext context, ViewPB view) {
|
||||
final iconData = view.icon.toEmojiIconData();
|
||||
Widget icon;
|
||||
if (iconData.isEmpty || iconData.type != FlowyIconType.icon) {
|
||||
icon = view.defaultIcon();
|
||||
} else {
|
||||
icon = RawEmojiIconWidget(
|
||||
emoji: iconData,
|
||||
emojiSize: 14.0,
|
||||
enableColor: false,
|
||||
);
|
||||
}
|
||||
return SizedBox.square(
|
||||
dimension: 20.0,
|
||||
child: icon,
|
||||
child: view.defaultIcon(),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+7
-47
@@ -1,16 +1,11 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/show_transition_bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
|
||||
import 'package:appflowy/plugins/database/application/database_controller.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/workspace/application/view/view_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_service.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
@@ -53,42 +48,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
context.pop();
|
||||
}
|
||||
}),
|
||||
const MobileQuickActionDivider(),
|
||||
_actionButton(
|
||||
context,
|
||||
_Action.changeIcon,
|
||||
() {
|
||||
showMobileBottomSheet(
|
||||
context,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
showHeader: true,
|
||||
title: LocaleKeys.titleBar_pageIcon.tr(),
|
||||
backgroundColor: AFThemeExtension.of(context).background,
|
||||
enableDraggableScrollable: true,
|
||||
minChildSize: 0.6,
|
||||
initialChildSize: 0.61,
|
||||
scrollableWidgetBuilder: (_, controller) {
|
||||
return Expanded(
|
||||
child: FlowyIconEmojiPicker(
|
||||
tabs: const [PickerTabType.icon],
|
||||
enableBackgroundColorSelection: false,
|
||||
onSelectedEmoji: (r) {
|
||||
ViewBackendService.updateViewIcon(
|
||||
viewId: view.id,
|
||||
viewIcon: r.data,
|
||||
);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
builder: (_) => const SizedBox.shrink(),
|
||||
).then((_) => Navigator.pop(context));
|
||||
},
|
||||
!isInline,
|
||||
),
|
||||
const MobileQuickActionDivider(),
|
||||
_divider(),
|
||||
_actionButton(
|
||||
context,
|
||||
_Action.duplicate,
|
||||
@@ -98,7 +58,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
},
|
||||
!isInline,
|
||||
),
|
||||
const MobileQuickActionDivider(),
|
||||
_divider(),
|
||||
_actionButton(
|
||||
context,
|
||||
_Action.delete,
|
||||
@@ -108,6 +68,7 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
},
|
||||
!isInline,
|
||||
),
|
||||
_divider(),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -127,20 +88,20 @@ class MobileDatabaseViewQuickActions extends StatelessWidget {
|
||||
enable: enable,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _divider() => const Divider(height: 8.5, thickness: 0.5);
|
||||
}
|
||||
|
||||
enum _Action {
|
||||
edit,
|
||||
changeIcon,
|
||||
delete,
|
||||
duplicate;
|
||||
duplicate,
|
||||
delete;
|
||||
|
||||
String get label {
|
||||
return switch (this) {
|
||||
edit => LocaleKeys.grid_settings_editView.tr(),
|
||||
duplicate => LocaleKeys.button_duplicate.tr(),
|
||||
delete => LocaleKeys.button_delete.tr(),
|
||||
changeIcon => LocaleKeys.disclosureAction_changeIcon.tr(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,7 +110,6 @@ enum _Action {
|
||||
edit => FlowySvgs.view_item_rename_s,
|
||||
duplicate => FlowySvgs.duplicate_s,
|
||||
delete => FlowySvgs.trash_s,
|
||||
changeIcon => FlowySvgs.change_icon_s,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import 'package:appflowy/mobile/presentation/base/mobile_view_page.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
@@ -11,7 +10,6 @@ class MobileDocumentScreen extends StatelessWidget {
|
||||
this.showMoreButton = true,
|
||||
this.fixedTitle,
|
||||
this.blockId,
|
||||
this.tabs = const [PickerTabType.emoji, PickerTabType.icon],
|
||||
});
|
||||
|
||||
/// view id
|
||||
@@ -20,7 +18,6 @@ class MobileDocumentScreen extends StatelessWidget {
|
||||
final bool showMoreButton;
|
||||
final String? fixedTitle;
|
||||
final String? blockId;
|
||||
final List<PickerTabType> tabs;
|
||||
|
||||
static const routeName = '/docs';
|
||||
static const viewId = 'id';
|
||||
@@ -28,7 +25,6 @@ class MobileDocumentScreen extends StatelessWidget {
|
||||
static const viewShowMoreButton = 'show_more_button';
|
||||
static const viewFixedTitle = 'fixed_title';
|
||||
static const viewBlockId = 'block_id';
|
||||
static const viewSelectTabs = 'select_tabs';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -39,7 +35,6 @@ class MobileDocumentScreen extends StatelessWidget {
|
||||
showMoreButton: showMoreButton,
|
||||
fixedTitle: fixedTitle,
|
||||
blockId: blockId,
|
||||
tabs: tabs,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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/shared/loading.dart';
|
||||
import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/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
-2
@@ -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/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
|
||||
import 'package:appflowy/plugins/base/emoji/emoji_text.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,7 +110,10 @@ class MobileRecentView extends StatelessWidget {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: state.icon.isNotEmpty
|
||||
? RawEmojiIconWidget(emoji: state.icon, emojiSize: 30)
|
||||
? EmojiText(
|
||||
emoji: state.icon.emoji,
|
||||
fontSize: 30.0,
|
||||
)
|
||||
: SizedBox.square(
|
||||
dimension: 32.0,
|
||||
child: view.defaultIcon(),
|
||||
|
||||
+1
-9
@@ -3,7 +3,6 @@ import 'package:appflowy/mobile/application/mobile_router.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_bloc.dart';
|
||||
@@ -102,14 +101,7 @@ class _Pages extends StatelessWidget {
|
||||
level: 0,
|
||||
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||
isFeedback: false,
|
||||
onSelected: (v) => context.pushView(
|
||||
v,
|
||||
tabs: [
|
||||
PickerTabType.emoji,
|
||||
PickerTabType.icon,
|
||||
PickerTabType.custom,
|
||||
].map((e) => e.name).toList(),
|
||||
),
|
||||
onSelected: context.pushView,
|
||||
endActionPane: (context) {
|
||||
final view = context.read<ViewBloc>().state.view;
|
||||
return buildEndActionPane(
|
||||
|
||||
+4
-13
@@ -11,7 +11,6 @@ import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emo
|
||||
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';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/util/string_extension.dart';
|
||||
import 'package:appflowy/util/theme_extension.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
@@ -82,14 +81,7 @@ class MobileViewPage extends StatelessWidget {
|
||||
spaceRatio: 4,
|
||||
),
|
||||
child: AnimatedGestureDetector(
|
||||
onTapUp: () => context.pushView(
|
||||
view,
|
||||
tabs: [
|
||||
PickerTabType.emoji,
|
||||
PickerTabType.icon,
|
||||
PickerTabType.custom,
|
||||
].map((e) => e.name).toList(),
|
||||
),
|
||||
onTapUp: () => context.pushView(view),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
@@ -148,8 +140,7 @@ class MobileViewPage extends StatelessWidget {
|
||||
final iconUrl = userProfile?.iconUrl;
|
||||
if (iconUrl == null ||
|
||||
iconUrl.isEmpty ||
|
||||
view.createdBy != userProfile?.id ||
|
||||
!isURL(iconUrl)) {
|
||||
view.createdBy != userProfile?.id) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
@@ -191,7 +182,7 @@ class MobileViewPage extends StatelessWidget {
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
child: RawEmojiIconWidget(
|
||||
child: EmojiIconWidget(
|
||||
emoji: icon,
|
||||
emojiSize: 18.0,
|
||||
),
|
||||
@@ -215,7 +206,7 @@ class MobileViewPage extends StatelessWidget {
|
||||
Widget _buildAuthor(BuildContext context, RecentViewState state) {
|
||||
return FlowyText.regular(
|
||||
// view.createdBy.toString(),
|
||||
'',
|
||||
'Lucas',
|
||||
fontSize: 12.0,
|
||||
color: Theme.of(context).hintColor,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -4,7 +4,6 @@ import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space_header.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/space/mobile_space_menu.dart';
|
||||
import 'package:appflowy/mobile/presentation/page_item/mobile_view_item.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/shared/list_extension.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
@@ -152,14 +151,7 @@ class _Pages extends StatelessWidget {
|
||||
level: 0,
|
||||
leftPadding: HomeSpaceViewSizes.leftPadding,
|
||||
isFeedback: false,
|
||||
onSelected: (v) => context.pushView(
|
||||
v,
|
||||
tabs: [
|
||||
PickerTabType.emoji,
|
||||
PickerTabType.icon,
|
||||
PickerTabType.custom,
|
||||
].map((e) => e.name).toList(),
|
||||
),
|
||||
onSelected: context.pushView,
|
||||
endActionPane: (context) {
|
||||
final view = context.read<ViewBloc>().state.view;
|
||||
final actions = [
|
||||
|
||||
@@ -6,7 +6,6 @@ import 'package:appflowy/mobile/presentation/home/tab/_tab_bar.dart';
|
||||
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
|
||||
import 'package:appflowy/mobile/presentation/presentation.dart';
|
||||
import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';
|
||||
import 'package:appflowy/shared/icon_emoji_picker/tab.dart';
|
||||
import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
|
||||
import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
|
||||
@@ -83,14 +82,7 @@ class _MobileSpaceTabState extends State<MobileSpaceTab>
|
||||
listener: (context, state) {
|
||||
final lastCreatedPage = state.lastCreatedRootView;
|
||||
if (lastCreatedPage != null) {
|
||||
context.pushView(
|
||||
lastCreatedPage,
|
||||
tabs: [
|
||||
PickerTabType.emoji,
|
||||
PickerTabType.icon,
|
||||
PickerTabType.custom,
|
||||
].map((e) => e.name).toList(),
|
||||
);
|
||||
context.pushView(lastCreatedPage);
|
||||
}
|
||||
},
|
||||
),
|
||||
|
||||
@@ -300,8 +300,7 @@ class _SingleMobileInnerViewItemState extends State<SingleMobileInnerViewItem> {
|
||||
}
|
||||
|
||||
Widget _buildViewIcon() {
|
||||
final iconData = widget.view.icon.toEmojiIconData();
|
||||
final icon = iconData.isNotEmpty
|
||||
final icon = widget.view.icon.value.isNotEmpty
|
||||
? EmojiIconWidget(
|
||||
emoji: widget.view.icon.toEmojiIconData(),
|
||||
emojiSize: Platform.isAndroid ? 16.0 : 18.0,
|
||||
|
||||
+1
-1
@@ -19,7 +19,7 @@ class AppearanceSettingGroup extends StatelessWidget {
|
||||
settingItemList: const [
|
||||
ThemeSetting(),
|
||||
FontSetting(),
|
||||
DisplaySizeSetting(),
|
||||
TextScaleSetting(),
|
||||
RTLSetting(),
|
||||
],
|
||||
);
|
||||
|
||||
+22
-39
@@ -1,7 +1,6 @@
|
||||
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';
|
||||
@@ -18,15 +17,15 @@ class RTLSetting extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textDirection =
|
||||
context.watch<AppearanceSettingsCubit>().state.textDirection;
|
||||
final layoutDirection =
|
||||
context.watch<AppearanceSettingsCubit>().state.layoutDirection;
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_textDirection_label.tr(),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
_textDirectionLabelText(textDirection),
|
||||
_textDirectionLabelText(layoutDirection),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
@@ -40,33 +39,30 @@ 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: textDirection == AppFlowyTextDirection.ltr,
|
||||
onTap: () => applyTextDirectionAndPop(
|
||||
context,
|
||||
AppFlowyTextDirection.ltr,
|
||||
),
|
||||
isSelected: layoutDirection == LayoutDirection.ltrLayout,
|
||||
onTap: () {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setLayoutDirection(LayoutDirection.ltrLayout);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
FlowyOptionTile.checkbox(
|
||||
showTopBorder: false,
|
||||
text: LocaleKeys.settings_appearance_textDirection_rtl.tr(),
|
||||
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,
|
||||
),
|
||||
isSelected: layoutDirection == LayoutDirection.rtlLayout,
|
||||
onTap: () {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setLayoutDirection(LayoutDirection.rtlLayout);
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -76,26 +72,13 @@ class RTLSetting extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
String _textDirectionLabelText(AppFlowyTextDirection textDirection) {
|
||||
String _textDirectionLabelText(LayoutDirection? textDirection) {
|
||||
switch (textDirection) {
|
||||
case AppFlowyTextDirection.auto:
|
||||
return LocaleKeys.settings_appearance_textDirection_auto.tr();
|
||||
case AppFlowyTextDirection.rtl:
|
||||
case LayoutDirection.rtlLayout:
|
||||
return LocaleKeys.settings_appearance_textDirection_rtl.tr();
|
||||
case AppFlowyTextDirection.ltr:
|
||||
case LayoutDirection.ltrLayout:
|
||||
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);
|
||||
}
|
||||
}
|
||||
+23
-55
@@ -1,55 +1,39 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
|
||||
import 'package:appflowy/startup/startup.dart';
|
||||
import 'package:appflowy/startup/tasks/app_window_size_manager.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/hotkeys.dart';
|
||||
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/font_size_stepper.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:scaled_app/scaled_app.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../setting.dart';
|
||||
|
||||
const int _divisions = 4;
|
||||
const double _minMobileScaleFactor = 0.8;
|
||||
const double _maxMobileScaleFactor = 1.2;
|
||||
|
||||
class DisplaySizeSetting extends StatefulWidget {
|
||||
const DisplaySizeSetting({
|
||||
class TextScaleSetting extends StatelessWidget {
|
||||
const TextScaleSetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
State<DisplaySizeSetting> createState() => _DisplaySizeSettingState();
|
||||
}
|
||||
|
||||
class _DisplaySizeSettingState extends State<DisplaySizeSetting> {
|
||||
double scaleFactor = 1.0;
|
||||
final windowSizeManager = WindowSizeManager();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
windowSizeManager.getScaleFactor().then((v) {
|
||||
if (v != scaleFactor && mounted) {
|
||||
setState(() {
|
||||
scaleFactor = v;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final textScaleFactor =
|
||||
context.watch<AppearanceSettingsCubit>().state.textScaleFactor;
|
||||
return MobileSettingItem(
|
||||
name: LocaleKeys.settings_appearance_displaySize.tr(),
|
||||
name: LocaleKeys.settings_appearance_fontScaleFactor.tr(),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
FlowyText(
|
||||
scaleFactor.toStringAsFixed(2),
|
||||
// map the text scale factor to the 0-1
|
||||
// 0.8 - 0.0
|
||||
// 0.9 - 0.5
|
||||
// 1.0 - 1.0
|
||||
((_divisions + 1) * textScaleFactor - _divisions)
|
||||
.toStringAsFixed(2),
|
||||
color: theme.colorScheme.onSurface,
|
||||
),
|
||||
const Icon(Icons.chevron_right),
|
||||
@@ -61,15 +45,17 @@ class _DisplaySizeSettingState extends State<DisplaySizeSetting> {
|
||||
showHeader: true,
|
||||
showDragHandle: true,
|
||||
showDivider: false,
|
||||
title: LocaleKeys.settings_appearance_displaySize.tr(),
|
||||
title: LocaleKeys.settings_appearance_fontScaleFactor.tr(),
|
||||
builder: (context) {
|
||||
return FontSizeStepper(
|
||||
value: scaleFactor,
|
||||
minimumValue: _minMobileScaleFactor,
|
||||
maximumValue: _maxMobileScaleFactor,
|
||||
value: textScaleFactor,
|
||||
minimumValue: 0.8,
|
||||
maximumValue: 1.0,
|
||||
divisions: _divisions,
|
||||
onChanged: (newScaleFactor) async {
|
||||
await _setScale(newScaleFactor);
|
||||
onChanged: (newTextScaleFactor) {
|
||||
context
|
||||
.read<AppearanceSettingsCubit>()
|
||||
.setTextScaleFactor(newTextScaleFactor);
|
||||
},
|
||||
);
|
||||
},
|
||||
@@ -77,22 +63,4 @@ class _DisplaySizeSettingState extends State<DisplaySizeSetting> {
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _setScale(double value) async {
|
||||
if (FlowyRunner.currentMode == IntegrationMode.integrationTest) {
|
||||
// The integration test will fail if we check the scale factor in the test.
|
||||
// #0 ScaledWidgetsFlutterBinding.Eval ()
|
||||
// #1 ScaledWidgetsFlutterBinding.instance (package:scaled_app/scaled_app.dart:66:62)
|
||||
// ignore: invalid_use_of_visible_for_testing_member
|
||||
appflowyScaleFactor = value;
|
||||
} else {
|
||||
ScaledWidgetsFlutterBinding.instance.scaleFactor = (_) => value;
|
||||
}
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
scaleFactor = value;
|
||||
});
|
||||
}
|
||||
await windowSizeManager.setScaleFactor(value);
|
||||
}
|
||||
}
|
||||
+3
-17
@@ -6,20 +6,13 @@ 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();
|
||||
@@ -53,10 +46,8 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
|
||||
controller: _textFieldController,
|
||||
keyboardType: TextInputType.text,
|
||||
validator: (value) {
|
||||
if (value == null ||
|
||||
value.isEmpty ||
|
||||
validateUrl(value).isFailure) {
|
||||
return LocaleKeys.settings_menu_invalidCloudURLScheme.tr();
|
||||
if (value == null || value.isEmpty) {
|
||||
return LocaleKeys.settings_mobile_usernameEmptyError.tr();
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -83,12 +74,7 @@ class _SelfHostUrlBottomSheetState extends State<SelfHostUrlBottomSheet> {
|
||||
if (value.isNotEmpty) {
|
||||
validateUrl(value).fold(
|
||||
(url) async {
|
||||
switch (widget.type) {
|
||||
case SelfHostUrlBottomSheetType.shareDomain:
|
||||
await useBaseWebDomain(url);
|
||||
case SelfHostUrlBottomSheetType.cloudURL:
|
||||
await useSelfHostedAppFlowyCloudWithURL(url);
|
||||
}
|
||||
await useSelfHostedAppFlowyCloudWithURL(url);
|
||||
await runAppFlowy();
|
||||
},
|
||||
(err) => Log.error(err),
|
||||
|
||||
+25
-88
@@ -3,7 +3,6 @@ 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';
|
||||
@@ -18,106 +17,44 @@ class SelfHostSettingGroup extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SelfHostSettingGroupState extends State<SelfHostSettingGroup> {
|
||||
final future = Future.wait([
|
||||
getAppFlowyCloudUrl(),
|
||||
getAppFlowyShareDomain(),
|
||||
]);
|
||||
final future = getAppFlowyCloudUrl();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: future,
|
||||
builder: (context, snapshot) {
|
||||
final data = snapshot.data;
|
||||
if (!snapshot.hasData || data == null || data.length != 2) {
|
||||
if (!snapshot.hasData) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
final url = data[0];
|
||||
final shareDomain = data[1];
|
||||
final url = snapshot.data ?? '';
|
||||
return MobileSettingGroup(
|
||||
groupTitle: LocaleKeys.settings_menu_cloudAppFlowy.tr(),
|
||||
groupTitle: LocaleKeys.settings_menu_cloudAppFlowySelfHost.tr(),
|
||||
settingItemList: [
|
||||
_buildSelfHostField(url),
|
||||
_buildShareDomainField(shareDomain),
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
+17
-23
@@ -4,29 +4,41 @@ import 'package:flutter/material.dart';
|
||||
class MobileSettingItem extends StatelessWidget {
|
||||
const MobileSettingItem({
|
||||
super.key,
|
||||
this.name,
|
||||
required 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: title ?? _buildDefaultTitle(name),
|
||||
title: Row(
|
||||
children: [
|
||||
if (leadingIcon != null) ...[
|
||||
leadingIcon!,
|
||||
const HSpace(8),
|
||||
],
|
||||
Expanded(
|
||||
child: FlowyText.medium(
|
||||
name,
|
||||
fontSize: 14.0,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
subtitle: subtitle,
|
||||
trailing: trailing,
|
||||
onTap: onTap,
|
||||
@@ -35,22 +47,4 @@ 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
+27
-32
@@ -25,41 +25,36 @@ class MobileQuickActionButton extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final iconSize = this.iconSize ?? const Size.square(18);
|
||||
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,
|
||||
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: 44,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class MobileQuickActionDivider extends StatelessWidget {
|
||||
const MobileQuickActionDivider({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return const Divider(height: 0.5, thickness: 0.5);
|
||||
}
|
||||
}
|
||||
@@ -98,20 +98,6 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
onAIImageResponseLimit: () {
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageState: const MessageState.onAIImageResponseLimit(),
|
||||
),
|
||||
);
|
||||
},
|
||||
onAIMaxRequired: (message) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
messageState: MessageState.onAIMaxRequired(message),
|
||||
),
|
||||
);
|
||||
},
|
||||
receiveMetadata: (metadata) {
|
||||
Log.debug("AI Steps: ${metadata.progress?.step}");
|
||||
emit(
|
||||
@@ -143,22 +129,11 @@ class ChatAIMessageBloc extends Bloc<ChatAIMessageEvent, ChatAIMessageState> {
|
||||
add(const ChatAIMessageEvent.onAIResponseLimit());
|
||||
}
|
||||
},
|
||||
onAIImageResponseLimit: () {
|
||||
if (!isClosed) {
|
||||
add(const ChatAIMessageEvent.onAIImageResponseLimit());
|
||||
}
|
||||
},
|
||||
onMetadata: (metadata) {
|
||||
if (!isClosed) {
|
||||
add(ChatAIMessageEvent.receiveMetadata(metadata));
|
||||
}
|
||||
},
|
||||
onAIMaxRequired: (message) {
|
||||
if (!isClosed) {
|
||||
Log.info(message);
|
||||
add(ChatAIMessageEvent.onAIMaxRequired(message));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -170,10 +145,6 @@ class ChatAIMessageEvent with _$ChatAIMessageEvent {
|
||||
const factory ChatAIMessageEvent.retry() = _Retry;
|
||||
const factory ChatAIMessageEvent.retryResult(String text) = _RetryResult;
|
||||
const factory ChatAIMessageEvent.onAIResponseLimit() = _OnAIResponseLimit;
|
||||
const factory ChatAIMessageEvent.onAIImageResponseLimit() =
|
||||
_OnAIImageResponseLimit;
|
||||
const factory ChatAIMessageEvent.onAIMaxRequired(String message) =
|
||||
_OnAIMaxRquired;
|
||||
const factory ChatAIMessageEvent.receiveMetadata(
|
||||
MetadataCollection metadata,
|
||||
) = _ReceiveMetadata;
|
||||
@@ -207,8 +178,6 @@ class ChatAIMessageState with _$ChatAIMessageState {
|
||||
class MessageState with _$MessageState {
|
||||
const factory MessageState.onError(String error) = _Error;
|
||||
const factory MessageState.onAIResponseLimit() = _AIResponseLimit;
|
||||
const factory MessageState.onAIImageResponseLimit() = _AIImageResponseLimit;
|
||||
const factory MessageState.onAIMaxRequired(String message) = _AIMaxRequired;
|
||||
const factory MessageState.ready() = _Ready;
|
||||
const factory MessageState.loading() = _Loading;
|
||||
}
|
||||
@@ -59,8 +59,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
bool isLoadingPreviousMessages = false;
|
||||
bool hasMorePreviousMessages = true;
|
||||
AnswerStream? answerStream;
|
||||
bool isFetchingRelatedQuestions = false;
|
||||
bool shouldFetchRelatedQuestions = false;
|
||||
int numSendMessage = 0;
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
@@ -166,17 +165,13 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
},
|
||||
sendMessage: (
|
||||
String message,
|
||||
PredefinedFormat? format,
|
||||
Map<String, dynamic>? metadata,
|
||||
) {
|
||||
_clearErrorMessages(emit);
|
||||
_clearRelatedQuestions();
|
||||
_startStreamingMessage(message, format, metadata);
|
||||
lastSentMessage = null;
|
||||
numSendMessage += 1;
|
||||
|
||||
isFetchingRelatedQuestions = false;
|
||||
shouldFetchRelatedQuestions =
|
||||
format == null || format.imageFormat.hasText;
|
||||
_clearRelatedQuestions();
|
||||
_startStreamingMessage(message, metadata);
|
||||
lastSentMessage = null;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
@@ -236,14 +231,11 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
),
|
||||
);
|
||||
},
|
||||
regenerateAnswer: (id, format) {
|
||||
regenerateAnswer: (id) {
|
||||
_clearRelatedQuestions();
|
||||
_regenerateAnswer(id, format);
|
||||
_regenerateAnswer(id);
|
||||
lastSentMessage = null;
|
||||
|
||||
isFetchingRelatedQuestions = false;
|
||||
shouldFetchRelatedQuestions = false;
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
promptResponseState: PromptResponseState.sendingQuestion,
|
||||
@@ -266,9 +258,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
.send()
|
||||
.onFailure(Log.error);
|
||||
},
|
||||
deleteMessage: (mesesage) async {
|
||||
await chatController.remove(mesesage);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -330,9 +319,7 @@ 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 ||
|
||||
!shouldFetchRelatedQuestions) {
|
||||
if (answerStream == null || lastSentMessage == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -341,19 +328,17 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
messageId: lastSentMessage!.messageId,
|
||||
);
|
||||
|
||||
isFetchingRelatedQuestions = true;
|
||||
// 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;
|
||||
await AIEventGetRelatedQuestion(payload).send().fold(
|
||||
(list) {
|
||||
// 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) {
|
||||
if (!isClosed && preNumSendMessage == numSendMessage) {
|
||||
add(
|
||||
ChatEvent.didReceiveRelatedQuestions(
|
||||
list.items.map((e) => e.content).toList(),
|
||||
),
|
||||
);
|
||||
isFetchingRelatedQuestions = false;
|
||||
}
|
||||
},
|
||||
(err) => Log.error("Failed to get related questions: $err"),
|
||||
@@ -413,7 +398,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
|
||||
Future<void> _startStreamingMessage(
|
||||
String message,
|
||||
PredefinedFormat? format,
|
||||
Map<String, dynamic>? metadata,
|
||||
) async {
|
||||
await answerStream?.dispose();
|
||||
@@ -436,9 +420,6 @@ 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(
|
||||
@@ -479,10 +460,7 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
);
|
||||
}
|
||||
|
||||
void _regenerateAnswer(
|
||||
String answerMessageIdString,
|
||||
PredefinedFormat? format,
|
||||
) async {
|
||||
void _regenerateAnswer(String answerMessageIdString) async {
|
||||
final id = temporaryMessageIDMap.entries
|
||||
.firstWhereOrNull((e) => e.value == answerMessageIdString)
|
||||
?.key ??
|
||||
@@ -501,9 +479,6 @@ class ChatBloc extends Bloc<ChatEvent, ChatState> {
|
||||
answerMessageId: answerMessageId,
|
||||
answerStreamPort: Int64(answerStream!.nativePort),
|
||||
);
|
||||
if (format != null) {
|
||||
payload.format = format.toPB();
|
||||
}
|
||||
|
||||
await AIEventRegenerateResponse(payload).send().fold(
|
||||
(success) {
|
||||
@@ -583,21 +558,6 @@ 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(
|
||||
@@ -626,17 +586,13 @@ class ChatEvent with _$ChatEvent {
|
||||
// send message
|
||||
const factory ChatEvent.sendMessage({
|
||||
required String message,
|
||||
PredefinedFormat? format,
|
||||
Map<String, dynamic>? metadata,
|
||||
}) = _SendMessage;
|
||||
const factory ChatEvent.finishSending() = _FinishSendMessage;
|
||||
const factory ChatEvent.failedSending() = _FailSendMessage;
|
||||
|
||||
// regenerate
|
||||
const factory ChatEvent.regenerateAnswer(
|
||||
String id,
|
||||
PredefinedFormat? format,
|
||||
) = _RegenerateAnswer;
|
||||
const factory ChatEvent.regenerateAnswer(String id) = _RegenerateAnswer;
|
||||
|
||||
// streaming answer
|
||||
const factory ChatEvent.stopStream() = _StopStream;
|
||||
@@ -658,8 +614,6 @@ class ChatEvent with _$ChatEvent {
|
||||
const factory ChatEvent.didReceiveRelatedQuestions(
|
||||
List<String> questions,
|
||||
) = _DidReceiveRelatedQueston;
|
||||
|
||||
const factory ChatEvent.deleteMessage(Message message) = _DeleteMessage;
|
||||
}
|
||||
|
||||
@freezed
|
||||
@@ -668,14 +622,12 @@ class ChatState with _$ChatState {
|
||||
required List<String> selectedSourceIds,
|
||||
required LoadChatMessageStatus loadingState,
|
||||
required PromptResponseState promptResponseState,
|
||||
required bool clearErrorMessages,
|
||||
}) = _ChatState;
|
||||
|
||||
factory ChatState.initial() => const ChatState(
|
||||
selectedSourceIds: [],
|
||||
loadingState: LoadChatMessageStatus.loading,
|
||||
promptResponseState: PromptResponseState.ready,
|
||||
clearErrorMessages: false,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+6
-15
@@ -14,8 +14,6 @@ import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||
|
||||
class ChatEditDocumentService {
|
||||
const ChatEditDocumentService._();
|
||||
|
||||
static Future<ViewPB?> saveMessagesToNewPage(
|
||||
String chatPageName,
|
||||
String parentViewId,
|
||||
@@ -31,10 +29,7 @@ class ChatEditDocumentService {
|
||||
return null;
|
||||
}
|
||||
|
||||
final document = customMarkdownToDocument(
|
||||
completeMessage,
|
||||
tableWidth: 250.0,
|
||||
);
|
||||
final document = customMarkdownToDocument(completeMessage);
|
||||
final initialBytes =
|
||||
DocumentDataPBFromTo.fromDocument(document)?.writeToBuffer();
|
||||
if (initialBytes == null) {
|
||||
@@ -50,13 +45,12 @@ class ChatEditDocumentService {
|
||||
).toNullable();
|
||||
}
|
||||
|
||||
static Future<void> addMessagesToPage(
|
||||
static Future<void> addMessageToPage(
|
||||
String documentId,
|
||||
List<TextMessage> messages,
|
||||
TextMessage message,
|
||||
) async {
|
||||
// Convert messages to markdown and trim the last empty newline.
|
||||
final completeMessage = messages.map((m) => m.text).join('\n').trimRight();
|
||||
if (completeMessage.isEmpty) {
|
||||
if (message.text.isEmpty) {
|
||||
Log.error('Message is empty');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -75,10 +69,7 @@ class ChatEditDocumentService {
|
||||
return;
|
||||
}
|
||||
|
||||
final messageDocument = customMarkdownToDocument(
|
||||
completeMessage,
|
||||
tableWidth: 250.0,
|
||||
);
|
||||
final messageDocument = customMarkdownToDocument(message.text);
|
||||
if (messageDocument.isEmpty) {
|
||||
Log.error('Failed to convert message to document');
|
||||
return;
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import 'dart:io';
|
||||
|
||||
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-ai/entities.pbenum.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;
|
||||
@@ -140,87 +137,3 @@ enum LoadChatMessageStatus {
|
||||
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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -12,13 +12,13 @@ class ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {
|
||||
ChatMemberBloc() : super(const ChatMemberState()) {
|
||||
on<ChatMemberEvent>(
|
||||
(event, emit) async {
|
||||
await event.when(
|
||||
event.when(
|
||||
receiveMemberInfo: (String id, WorkspaceMemberPB memberInfo) {
|
||||
final members = Map<String, ChatMember>.from(state.members);
|
||||
members[id] = ChatMember(info: memberInfo);
|
||||
emit(state.copyWith(members: members));
|
||||
},
|
||||
getMemberInfo: (String userId) async {
|
||||
getMemberInfo: (String userId) {
|
||||
if (state.members.containsKey(userId)) {
|
||||
// Member info already exists. Debouncing refresh member info from backend would be better.
|
||||
return;
|
||||
@@ -27,15 +27,19 @@ class ChatMemberBloc extends Bloc<ChatMemberEvent, ChatMemberState> {
|
||||
final payload = WorkspaceMemberIdPB(
|
||||
uid: Int64.parseInt(userId),
|
||||
);
|
||||
await UserEventGetMemberInfo(payload).send().then((result) {
|
||||
result.fold(
|
||||
(member) {
|
||||
if (!isClosed) {
|
||||
add(ChatMemberEvent.receiveMemberInfo(userId, member));
|
||||
}
|
||||
},
|
||||
(err) => Log.error("Error getting member info: $err"),
|
||||
);
|
||||
UserEventGetMemberInfo(payload).send().then((result) {
|
||||
if (!isClosed) {
|
||||
result.fold((member) {
|
||||
add(
|
||||
ChatMemberEvent.receiveMemberInfo(
|
||||
userId,
|
||||
member,
|
||||
),
|
||||
);
|
||||
}, (err) {
|
||||
Log.error("Error getting member info: $err");
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
@@ -25,24 +25,12 @@ class AnswerStream {
|
||||
} else if (event == "AI_RESPONSE_LIMIT") {
|
||||
_aiLimitReached = true;
|
||||
_onAIResponseLimit?.call();
|
||||
} else if (event == "AI_IMAGE_RESPONSE_LIMIT") {
|
||||
_aiImageLimitReached = true;
|
||||
_onAIImageResponseLimit?.call();
|
||||
} else if (event.startsWith("AI_MAX_REQUIRED:")) {
|
||||
final msg = event.substring(16);
|
||||
// If the callback is not registered yet, add the event to the buffer.
|
||||
if (_onAIMaxRequired != null) {
|
||||
_onAIMaxRequired!(msg);
|
||||
} else {
|
||||
_pendingAIMaxRequiredEvents.add(msg);
|
||||
}
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
_onEnd?.call();
|
||||
},
|
||||
onError: (error) {
|
||||
_error = error.toString();
|
||||
_onError?.call(error.toString());
|
||||
},
|
||||
);
|
||||
@@ -53,7 +41,6 @@ class AnswerStream {
|
||||
late StreamSubscription<String> _subscription;
|
||||
bool _hasStarted = false;
|
||||
bool _aiLimitReached = false;
|
||||
bool _aiImageLimitReached = false;
|
||||
String? _error;
|
||||
String _text = "";
|
||||
|
||||
@@ -63,17 +50,11 @@ class AnswerStream {
|
||||
void Function()? _onEnd;
|
||||
void Function(String error)? _onError;
|
||||
void Function()? _onAIResponseLimit;
|
||||
void Function()? _onAIImageResponseLimit;
|
||||
void Function(String message)? _onAIMaxRequired;
|
||||
void Function(MetadataCollection metadataCollection)? _onMetadata;
|
||||
|
||||
// Buffer for events that occur before listen() is called.
|
||||
final List<String> _pendingAIMaxRequiredEvents = [];
|
||||
|
||||
int get nativePort => _port.sendPort.nativePort;
|
||||
bool get hasStarted => _hasStarted;
|
||||
bool get aiLimitReached => _aiLimitReached;
|
||||
bool get aiImageLimitReached => _aiImageLimitReached;
|
||||
String? get error => _error;
|
||||
String get text => _text;
|
||||
|
||||
@@ -89,8 +70,6 @@ class AnswerStream {
|
||||
void Function()? onEnd,
|
||||
void Function(String error)? onError,
|
||||
void Function()? onAIResponseLimit,
|
||||
void Function()? onAIImageResponseLimit,
|
||||
void Function(String message)? onAIMaxRequired,
|
||||
void Function(MetadataCollection metadata)? onMetadata,
|
||||
}) {
|
||||
_onData = onData;
|
||||
@@ -98,17 +77,7 @@ class AnswerStream {
|
||||
_onEnd = onEnd;
|
||||
_onError = onError;
|
||||
_onAIResponseLimit = onAIResponseLimit;
|
||||
_onAIImageResponseLimit = onAIImageResponseLimit;
|
||||
_onMetadata = onMetadata;
|
||||
_onAIMaxRequired = onAIMaxRequired;
|
||||
|
||||
// Flush any buffered AI_MAX_REQUIRED events.
|
||||
if (_onAIMaxRequired != null && _pendingAIMaxRequiredEvents.isNotEmpty) {
|
||||
for (final msg in _pendingAIMaxRequiredEvents) {
|
||||
_onAIMaxRequired!(msg);
|
||||
}
|
||||
_pendingAIMaxRequiredEvents.clear();
|
||||
}
|
||||
|
||||
_onStart?.call();
|
||||
}
|
||||
|
||||
-96
@@ -1,96 +0,0 @@
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_entity.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_chat_core/flutter_chat_core.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'chat_select_message_bloc.freezed.dart';
|
||||
|
||||
class ChatSelectMessageBloc
|
||||
extends Bloc<ChatSelectMessageEvent, ChatSelectMessageState> {
|
||||
ChatSelectMessageBloc({required this.viewNotifier})
|
||||
: super(ChatSelectMessageState.initial()) {
|
||||
_dispatch();
|
||||
}
|
||||
|
||||
final ViewPluginNotifier viewNotifier;
|
||||
|
||||
void _dispatch() {
|
||||
on<ChatSelectMessageEvent>(
|
||||
(event, emit) {
|
||||
event.when(
|
||||
toggleSelectingMessages: () {
|
||||
if (state.isSelectingMessages) {
|
||||
emit(ChatSelectMessageState.initial());
|
||||
} else {
|
||||
emit(state.copyWith(isSelectingMessages: true));
|
||||
}
|
||||
},
|
||||
toggleSelectMessage: (Message message) {
|
||||
if (state.selectedMessages.contains(message)) {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedMessages: state.selectedMessages
|
||||
.where((m) => m != message)
|
||||
.toList(),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
selectedMessages: [...state.selectedMessages, message],
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
selectAllMessages: (List<Message> messages) {
|
||||
final filtered = messages.where(isAIMessage).toList();
|
||||
emit(state.copyWith(selectedMessages: filtered));
|
||||
},
|
||||
unselectAllMessages: () {
|
||||
emit(state.copyWith(selectedMessages: const []));
|
||||
},
|
||||
saveAsPage: () {
|
||||
emit(ChatSelectMessageState.initial());
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
bool isMessageSelected(String messageId) =>
|
||||
state.selectedMessages.any((m) => m.id == messageId);
|
||||
|
||||
bool isAIMessage(Message message) {
|
||||
return message.author.id == aiResponseUserId ||
|
||||
message.author.id == systemUserId ||
|
||||
message.author.id.startsWith("streamId:");
|
||||
}
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatSelectMessageEvent with _$ChatSelectMessageEvent {
|
||||
const factory ChatSelectMessageEvent.toggleSelectingMessages() =
|
||||
_ToggleSelectingMessages;
|
||||
const factory ChatSelectMessageEvent.toggleSelectMessage(Message message) =
|
||||
_ToggleSelectMessage;
|
||||
const factory ChatSelectMessageEvent.selectAllMessages(
|
||||
List<Message> messages,
|
||||
) = _SelectAllMessages;
|
||||
const factory ChatSelectMessageEvent.unselectAllMessages() =
|
||||
_UnselectAllMessages;
|
||||
const factory ChatSelectMessageEvent.saveAsPage() = _SaveAsPage;
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ChatSelectMessageState with _$ChatSelectMessageState {
|
||||
const factory ChatSelectMessageState({
|
||||
required bool isSelectingMessages,
|
||||
required List<Message> selectedMessages,
|
||||
}) = _ChatSelectMessageState;
|
||||
|
||||
factory ChatSelectMessageState.initial() => const ChatSelectMessageState(
|
||||
isSelectingMessages: false,
|
||||
selectedMessages: [],
|
||||
);
|
||||
}
|
||||
+6
-20
@@ -101,24 +101,18 @@ class ChatSource {
|
||||
}
|
||||
|
||||
class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
ChatSettingsCubit({
|
||||
this.hideDisabled = false,
|
||||
}) : super(ChatSettingsState.initial());
|
||||
ChatSettingsCubit() : super(ChatSettingsState.initial());
|
||||
|
||||
final bool hideDisabled;
|
||||
|
||||
final List<String> selectedSourceIds = [];
|
||||
final List<ChatSource> sources = [];
|
||||
final List<ChatSource> selectedSources = [];
|
||||
List<String> selectedSourceIds = [];
|
||||
List<ChatSource> sources = [];
|
||||
List<ChatSource> selectedSources = [];
|
||||
String filter = '';
|
||||
|
||||
void updateSelectedSources(List<String> newSelectedSourceIds) {
|
||||
selectedSourceIds
|
||||
..clear
|
||||
..addAll(newSelectedSourceIds);
|
||||
selectedSourceIds = [...newSelectedSourceIds];
|
||||
}
|
||||
|
||||
void refreshSources(List<ViewPB> spaceViews, ViewPB? currentSpace) async {
|
||||
void refreshSources(List<ViewPB> spaceViews) async {
|
||||
filter = "";
|
||||
|
||||
final newSources = await Future.wait(
|
||||
@@ -127,11 +121,6 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
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))
|
||||
@@ -171,9 +160,6 @@ class ChatSettingsCubit extends Cubit<ChatSettingsState> {
|
||||
if (childView.layout == ViewLayoutPB.Chat) {
|
||||
continue;
|
||||
}
|
||||
if (childView.layout != ViewLayoutPB.Document && hideDisabled) {
|
||||
continue;
|
||||
}
|
||||
final childChatSource = await _recursiveBuild(childView, view);
|
||||
if (childChatSource.selectedStatus.isSelected) {
|
||||
selectedCount++;
|
||||
|
||||
@@ -1,22 +1,14 @@
|
||||
import 'package:appflowy/generated/flowy_svgs.g.dart';
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/application/chat_select_message_bloc.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/chat_page.dart';
|
||||
import 'package:appflowy/plugins/util.dart';
|
||||
import 'package:appflowy/startup/plugin/plugin.dart';
|
||||
import 'package:appflowy/workspace/application/view/view_ext.dart';
|
||||
import 'package:appflowy/workspace/application/view_info/view_info_bloc.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/home_stack.dart';
|
||||
import 'package:appflowy/workspace/presentation/home/menu/view/view_action_type.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/favorite_button.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/more_view_actions.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/more_view_actions/widgets/common_view_action.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/tab_bar_item.dart';
|
||||
import 'package:appflowy/workspace/presentation/widgets/view_title_bar.dart';
|
||||
import 'package:appflowy_backend/log.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:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
@@ -54,16 +46,13 @@ class AIChatPagePlugin extends Plugin {
|
||||
}) : notifier = ViewPluginNotifier(view: view);
|
||||
|
||||
late final ViewInfoBloc _viewInfoBloc;
|
||||
late final _chatMessageSelectorBloc =
|
||||
ChatSelectMessageBloc(viewNotifier: notifier);
|
||||
|
||||
@override
|
||||
final ViewPluginNotifier notifier;
|
||||
|
||||
@override
|
||||
PluginWidgetBuilder get widgetBuilder => AIChatPagePluginWidgetBuilder(
|
||||
viewInfoBloc: _viewInfoBloc,
|
||||
chatMessageSelectorBloc: _chatMessageSelectorBloc,
|
||||
bloc: _viewInfoBloc,
|
||||
notifier: notifier,
|
||||
);
|
||||
|
||||
@@ -82,7 +71,6 @@ class AIChatPagePlugin extends Plugin {
|
||||
@override
|
||||
void dispose() {
|
||||
_viewInfoBloc.close();
|
||||
_chatMessageSelectorBloc.close();
|
||||
notifier.dispose();
|
||||
}
|
||||
}
|
||||
@@ -90,13 +78,11 @@ class AIChatPagePlugin extends Plugin {
|
||||
class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
with NavigationItem {
|
||||
AIChatPagePluginWidgetBuilder({
|
||||
required this.viewInfoBloc,
|
||||
required this.chatMessageSelectorBloc,
|
||||
required this.bloc,
|
||||
required this.notifier,
|
||||
});
|
||||
|
||||
final ViewInfoBloc viewInfoBloc;
|
||||
final ChatSelectMessageBloc chatMessageSelectorBloc;
|
||||
final ViewInfoBloc bloc;
|
||||
final ViewPluginNotifier notifier;
|
||||
int? deletedViewIndex;
|
||||
|
||||
@@ -124,11 +110,8 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
return const SizedBox();
|
||||
}
|
||||
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: chatMessageSelectorBloc),
|
||||
BlocProvider.value(value: viewInfoBloc),
|
||||
],
|
||||
return BlocProvider.value(
|
||||
value: bloc,
|
||||
child: AIChatPage(
|
||||
userProfile: context.userProfile!,
|
||||
key: ValueKey(notifier.view.id),
|
||||
@@ -151,51 +134,4 @@ class AIChatPagePluginWidgetBuilder extends PluginWidgetBuilder
|
||||
|
||||
@override
|
||||
EdgeInsets get contentPadding => EdgeInsets.zero;
|
||||
|
||||
@override
|
||||
Widget? get rightBarItem => MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: viewInfoBloc),
|
||||
BlocProvider.value(value: chatMessageSelectorBloc),
|
||||
],
|
||||
child: BlocBuilder<ChatSelectMessageBloc, ChatSelectMessageState>(
|
||||
builder: (context, state) {
|
||||
if (state.isSelectingMessages) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ViewFavoriteButton(
|
||||
key: ValueKey('favorite_button_${notifier.view.id}'),
|
||||
view: notifier.view,
|
||||
),
|
||||
const HSpace(4),
|
||||
MoreViewActions(
|
||||
key: ValueKey(notifier.view.id),
|
||||
view: notifier.view,
|
||||
customActions: [
|
||||
CustomViewAction(
|
||||
view: notifier.view,
|
||||
leftIcon: FlowySvgs.ai_add_to_page_s,
|
||||
label: LocaleKeys.moreAction_saveAsNewPage.tr(),
|
||||
onTap: () {
|
||||
chatMessageSelectorBloc.add(
|
||||
const ChatSelectMessageEvent
|
||||
.toggleSelectingMessages(),
|
||||
);
|
||||
},
|
||||
),
|
||||
ViewAction(
|
||||
type: ViewMoreActionType.divider,
|
||||
view: notifier.view,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||
import 'package:appflowy/plugins/ai_chat/presentation/chat_message_selector_banner.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';
|
||||
@@ -23,11 +22,10 @@ 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_select_message_bloc.dart';
|
||||
import 'application/chat_message_stream.dart';
|
||||
import 'presentation/animated_chat_list.dart';
|
||||
import 'presentation/chat_input/desktop_chat_input.dart';
|
||||
import 'presentation/chat_input/mobile_chat_input.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';
|
||||
@@ -108,22 +106,20 @@ class _ChatContentPage extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (context, state) {
|
||||
return switch (state.loadingState) {
|
||||
LoadChatMessageStatus.ready => Column(
|
||||
children: [
|
||||
ChatMessageSelectorBanner(
|
||||
view: view,
|
||||
allMessages: context.read<ChatBloc>().chatController.messages,
|
||||
),
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: _wrapConstraints(
|
||||
ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context)
|
||||
.copyWith(scrollbars: false),
|
||||
return Center(
|
||||
child: Container(
|
||||
constraints: const BoxConstraints(maxWidth: 784),
|
||||
margin: UniversalPlatform.isDesktop
|
||||
? const EdgeInsets.symmetric(horizontal: 60.0)
|
||||
: null,
|
||||
child: ScrollConfiguration(
|
||||
behavior: ScrollConfiguration.of(context).copyWith(scrollbars: false),
|
||||
child: BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (context, state) {
|
||||
return switch (state.loadingState) {
|
||||
LoadChatMessageStatus.ready => Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Chat(
|
||||
chatController:
|
||||
context.read<ChatBloc>().chatController,
|
||||
@@ -139,27 +135,15 @@ class _ChatContentPage extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildInput(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
_wrapConstraints(
|
||||
_builtInput(context),
|
||||
),
|
||||
],
|
||||
),
|
||||
_ => const Center(child: CircularProgressIndicator.adaptive()),
|
||||
};
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _wrapConstraints(Widget child) {
|
||||
return Container(
|
||||
constraints: const BoxConstraints(maxWidth: 784),
|
||||
margin: UniversalPlatform.isDesktop
|
||||
? const EdgeInsets.symmetric(horizontal: 60.0)
|
||||
: null,
|
||||
child: child,
|
||||
_ => const Center(child: CircularProgressIndicator.adaptive()),
|
||||
};
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -209,38 +193,28 @@ class _ChatContentPage extends StatelessWidget {
|
||||
final refSourceJsonString =
|
||||
message.metadata?[messageRefSourceJsonStringKey] as String?;
|
||||
|
||||
return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(
|
||||
selector: (state) => state.isSelectingMessages,
|
||||
builder: (context, isSelectingMessages) {
|
||||
return BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (context, state) {
|
||||
final chatController = context.read<ChatBloc>().chatController;
|
||||
final messages = chatController.messages
|
||||
.where((e) => onetimeMessageTypeFromMeta(e.metadata) == null);
|
||||
final isLastMessage =
|
||||
messages.isEmpty ? false : messages.last.id == message.id;
|
||||
return ChatAIMessageWidget(
|
||||
user: message.author,
|
||||
messageUserId: message.id,
|
||||
message: message,
|
||||
stream: stream is AnswerStream ? stream : null,
|
||||
questionId: questionId,
|
||||
chatId: view.id,
|
||||
refSourceJsonString: refSourceJsonString,
|
||||
isStreaming:
|
||||
state.promptResponseState != PromptResponseState.ready,
|
||||
isLastMessage: isLastMessage,
|
||||
isSelectingMessages: isSelectingMessages,
|
||||
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)),
|
||||
);
|
||||
},
|
||||
return BlocBuilder<ChatBloc, ChatState>(
|
||||
builder: (context, state) {
|
||||
final chatController = context.read<ChatBloc>().chatController;
|
||||
final messages = chatController.messages
|
||||
.where((e) => onetimeMessageTypeFromMeta(e.metadata) == null);
|
||||
final isLastMessage =
|
||||
messages.isEmpty ? false : messages.last.id == message.id;
|
||||
return ChatAIMessageWidget(
|
||||
user: message.author,
|
||||
messageUserId: message.id,
|
||||
message: message,
|
||||
stream: stream is AnswerStream ? stream : null,
|
||||
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)),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -288,100 +262,72 @@ class _ChatContentPage extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(
|
||||
selector: (state) => state.isSelectingMessages,
|
||||
builder: (context, isSelectingMessages) {
|
||||
return ChatAnimatedListReversed(
|
||||
scrollController: scrollController,
|
||||
itemBuilder: itemBuilder,
|
||||
bottomPadding: isSelectingMessages
|
||||
? 48.0 + DesktopAIChatSizes.messageActionBarIconSize
|
||||
: 8.0,
|
||||
onLoadPreviousMessages: () {
|
||||
bloc.add(const ChatEvent.loadPreviousMessages());
|
||||
},
|
||||
);
|
||||
return ChatAnimatedListReversed(
|
||||
scrollController: scrollController,
|
||||
itemBuilder: itemBuilder,
|
||||
onLoadPreviousMessages: () {
|
||||
bloc.add(const ChatEvent.loadPreviousMessages());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _builtInput(BuildContext context) {
|
||||
return BlocSelector<ChatSelectMessageBloc, ChatSelectMessageState, bool>(
|
||||
selector: (state) => state.isSelectingMessages,
|
||||
builder: (context, isSelectingMessages) {
|
||||
return AnimatedSwitcher(
|
||||
duration: const Duration(milliseconds: 150),
|
||||
transitionBuilder: (child, animation) {
|
||||
return SizeTransition(
|
||||
sizeFactor: animation,
|
||||
axisAlignment: -1,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: isSelectingMessages
|
||||
? const SizedBox.shrink()
|
||||
: Padding(
|
||||
padding: AIChatUILayout.safeAreaInsets(context),
|
||||
child: BlocSelector<ChatBloc, ChatState, bool>(
|
||||
selector: (state) {
|
||||
return state.promptResponseState ==
|
||||
PromptResponseState.ready;
|
||||
},
|
||||
builder: (context, canSendMessage) {
|
||||
final chatBloc = context.read<ChatBloc>();
|
||||
Widget _buildInput(BuildContext context) {
|
||||
return Padding(
|
||||
padding: AIChatUILayout.safeAreaInsets(context),
|
||||
child: BlocSelector<ChatBloc, ChatState, bool>(
|
||||
selector: (state) {
|
||||
return state.promptResponseState == PromptResponseState.ready;
|
||||
},
|
||||
builder: (context, canSendMessage) {
|
||||
final chatBloc = context.read<ChatBloc>();
|
||||
|
||||
return UniversalPlatform.isDesktop
|
||||
? DesktopChatInput(
|
||||
chatId: view.id,
|
||||
isStreaming: !canSendMessage,
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: MobileChatInput(
|
||||
chatId: view.id,
|
||||
isStreaming: !canSendMessage,
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
return UniversalPlatform.isDesktop
|
||||
? DesktopAIPromptInput(
|
||||
chatId: view.id,
|
||||
isStreaming: !canSendMessage,
|
||||
onStopStreaming: () {
|
||||
chatBloc.add(const ChatEvent.stopStream());
|
||||
},
|
||||
onSubmitted: (text, metadata) {
|
||||
chatBloc.add(
|
||||
ChatEvent.sendMessage(
|
||||
message: text,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
},
|
||||
onUpdateSelectedSources: (ids) {
|
||||
chatBloc.add(
|
||||
ChatEvent.updateSelectedSources(
|
||||
selectedSourcesIds: ids,
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
: MobileAIPromptInput(
|
||||
chatId: view.id,
|
||||
isStreaming: !canSendMessage,
|
||||
onStopStreaming: () {
|
||||
chatBloc.add(const ChatEvent.stopStream());
|
||||
},
|
||||
onSubmitted: (text, metadata) {
|
||||
chatBloc.add(
|
||||
ChatEvent.sendMessage(
|
||||
message: text,
|
||||
metadata: metadata,
|
||||
),
|
||||
);
|
||||
},
|
||||
onUpdateSelectedSources: (ids) {
|
||||
chatBloc.add(
|
||||
ChatEvent.updateSelectedSources(
|
||||
selectedSourcesIds: ids,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ class ChatAnimatedListReversedState extends State<ChatAnimatedListReversed>
|
||||
event.message != null,
|
||||
'Message must be provided when inserting a message.',
|
||||
);
|
||||
_onInserted(event.index!, event.message!);
|
||||
_onInserted(0, 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,15 +311,14 @@ 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 (position == 0 &&
|
||||
_userHasScrolled &&
|
||||
widget.scrollController.offset >
|
||||
if (_userHasScrolled &&
|
||||
widget.scrollController.offset >=
|
||||
widget.scrollController.position.minScrollExtent) {
|
||||
_userHasScrolled = false;
|
||||
}
|
||||
|
||||
_listKey.currentState!.insertItem(
|
||||
0,
|
||||
position,
|
||||
duration: widget.insertAnimationDuration,
|
||||
);
|
||||
|
||||
|
||||
@@ -10,13 +10,15 @@ import 'package:string_validator/string_validator.dart';
|
||||
import 'layout_define.dart';
|
||||
|
||||
class ChatAIAvatar extends StatelessWidget {
|
||||
const ChatAIAvatar({super.key});
|
||||
const ChatAIAvatar({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: DesktopAIChatSizes.avatarSize,
|
||||
height: DesktopAIChatSizes.avatarSize,
|
||||
width: DesktopAIConvoSizes.avatarSize,
|
||||
height: DesktopAIConvoSizes.avatarSize,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(shape: BoxShape.circle),
|
||||
foregroundDecoration: ShapeDecoration(
|
||||
@@ -59,8 +61,8 @@ class ChatUserAvatar extends StatelessWidget {
|
||||
child = _buildEmojiAvatar(context);
|
||||
}
|
||||
return Container(
|
||||
width: DesktopAIChatSizes.avatarSize,
|
||||
height: DesktopAIChatSizes.avatarSize,
|
||||
width: DesktopAIConvoSizes.avatarSize,
|
||||
height: DesktopAIConvoSizes.avatarSize,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: const BoxDecoration(shape: BoxShape.circle),
|
||||
foregroundDecoration: ShapeDecoration(
|
||||
@@ -100,7 +102,7 @@ class ChatUserAvatar extends StatelessWidget {
|
||||
Widget _buildUrlAvatar(BuildContext context) {
|
||||
return CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
radius: DesktopAIChatSizes.avatarSize / 2,
|
||||
radius: DesktopAIConvoSizes.avatarSize / 2,
|
||||
child: Image.network(
|
||||
iconUrl,
|
||||
fit: BoxFit.cover,
|
||||
@@ -113,7 +115,7 @@ class ChatUserAvatar extends StatelessWidget {
|
||||
Widget _buildEmojiAvatar(BuildContext context) {
|
||||
return CircleAvatar(
|
||||
backgroundColor: Colors.transparent,
|
||||
radius: DesktopAIChatSizes.avatarSize / 2,
|
||||
radius: DesktopAIConvoSizes.avatarSize / 2,
|
||||
child: builtInSVGIcons.contains(iconUrl)
|
||||
? FlowySvg(
|
||||
FlowySvgData('emoji/$iconUrl'),
|
||||
|
||||
+57
-1
@@ -6,7 +6,9 @@ import 'package:flowy_infra/theme_extension.dart';
|
||||
import 'package:flowy_infra_ui/style_widget/icon_button.dart';
|
||||
import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
|
||||
|
||||
import 'layout_define.dart';
|
||||
import '../layout_define.dart';
|
||||
|
||||
enum SendButtonState { enabled, streaming, disabled }
|
||||
|
||||
class PromptInputAttachmentButton extends StatelessWidget {
|
||||
const PromptInputAttachmentButton({required this.onTap, super.key});
|
||||
@@ -65,3 +67,57 @@ class PromptInputMentionButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PromptInputSendButton extends StatelessWidget {
|
||||
const PromptInputSendButton({
|
||||
super.key,
|
||||
required this.buttonSize,
|
||||
required this.iconSize,
|
||||
required this.state,
|
||||
required this.onSendPressed,
|
||||
required this.onStopStreaming,
|
||||
});
|
||||
|
||||
final double buttonSize;
|
||||
final double iconSize;
|
||||
final SendButtonState state;
|
||||
final VoidCallback onSendPressed;
|
||||
final VoidCallback onStopStreaming;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FlowyIconButton(
|
||||
width: buttonSize,
|
||||
icon: switch (state) {
|
||||
SendButtonState.enabled => FlowySvg(
|
||||
FlowySvgs.ai_send_filled_s,
|
||||
size: Size.square(iconSize),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
SendButtonState.disabled => FlowySvg(
|
||||
FlowySvgs.ai_send_filled_s,
|
||||
size: Size.square(iconSize),
|
||||
color: Theme.of(context).disabledColor,
|
||||
),
|
||||
SendButtonState.streaming => FlowySvg(
|
||||
FlowySvgs.ai_stop_filled_s,
|
||||
size: Size.square(iconSize),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
},
|
||||
onPressed: () {
|
||||
switch (state) {
|
||||
case SendButtonState.enabled:
|
||||
onSendPressed();
|
||||
break;
|
||||
case SendButtonState.streaming:
|
||||
onStopStreaming();
|
||||
break;
|
||||
case SendButtonState.disabled:
|
||||
break;
|
||||
}
|
||||
},
|
||||
hoverColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
}
|
||||
+3
-3
@@ -8,10 +8,10 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
import 'layout_define.dart';
|
||||
import '../layout_define.dart';
|
||||
|
||||
class PromptInputFile extends StatelessWidget {
|
||||
const PromptInputFile({
|
||||
class ChatInputFile extends StatelessWidget {
|
||||
const ChatInputFile({
|
||||
super.key,
|
||||
required this.chatId,
|
||||
required this.onDeleted,
|
||||
+2
-2
@@ -5,8 +5,8 @@ import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:extended_text_library/extended_text_library.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PromptInputTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||
PromptInputTextSpanBuilder({
|
||||
class ChatInputTextSpanBuilder extends SpecialTextSpanBuilder {
|
||||
ChatInputTextSpanBuilder({
|
||||
required this.inputControlCubit,
|
||||
this.specialTextStyle,
|
||||
});
|
||||
Loaded 100 of 3033 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user