diff --git a/.github/workflows/docker_ci.yml b/.github/workflows/docker_ci.yml
index 2c2143c1fee68..e91a82af16902 100644
--- a/.github/workflows/docker_ci.yml
+++ b/.github/workflows/docker_ci.yml
@@ -13,7 +13,7 @@ on:
- release/*
paths:
- frontend/**
- types: [opened, synchronize, reopened, unlocked, ready_for_review]
+ types: [ opened, synchronize, reopened, unlocked, ready_for_review ]
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
diff --git a/.github/workflows/flutter_ci.yaml b/.github/workflows/flutter_ci.yaml
index c1cdd7bc177a1..9b9725a09f567 100644
--- a/.github/workflows/flutter_ci.yaml
+++ b/.github/workflows/flutter_ci.yaml
@@ -246,12 +246,27 @@ jobs:
- name: Run Docker-Compose
working-directory: AppFlowy-Cloud
env:
- BACKEND_VERSION: 0.3.24-amd64
+ APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
run: |
- docker compose down -v --remove-orphans
- docker compose pull
- docker compose up -d
- sleep 10
+ container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
+ if [ -z "$container_id" ]; then
+ echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
+ docker compose pull
+ docker compose up -d
+ echo "Waiting for the container to be ready..."
+ sleep 10
+ else
+ running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
+ if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
+ echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
+ docker compose pull
+ docker compose up -d
+ echo "Waiting for the container to be ready..."
+ sleep 10
+ else
+ echo "AppFlowy-Cloud is running with the correct version."
+ fi
+ fi
- name: Checkout source code
uses: actions/checkout@v4
diff --git a/.github/workflows/ios_ci.yaml b/.github/workflows/ios_ci.yaml
index d24eaed1f3b10..e1be95894d7fa 100644
--- a/.github/workflows/ios_ci.yaml
+++ b/.github/workflows/ios_ci.yaml
@@ -21,27 +21,42 @@ on:
env:
FLUTTER_VERSION: "3.22.0"
- RUST_TOOLCHAIN: "1.77.2"
+ RUST_TOOLCHAIN: "1.80.1"
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
- build:
- if: github.event.pull_request.draft != true
- strategy:
- fail-fast: true
- matrix:
- os: [ macos-14 ]
- runs-on: ${{ matrix.os }}
+ build-self-hosted:
+ if: github.event.pull_request.head.repo.full_name == github.repository
+ runs-on: self-hosted
+
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v2
+
+ - name: Build AppFlowy
+ working-directory: frontend
+ run: |
+ cargo make --profile development-ios-arm64-sim appflowy-core-dev-ios
+ cargo make --profile development-ios-arm64-sim code_generation
+
+ - uses: futureware-tech/simulator-action@v3
+ id: simulator-action
+ with:
+ model: "iPhone 15"
+ shutdown_after_job: false
+
+ build-macos:
+ if: github.event.pull_request.head.repo.full_name != github.repository
+ runs-on: macos-13
steps:
- name: Checkout source code
uses: actions/checkout@v2
- name: Install Rust toolchain
- id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
@@ -49,8 +64,7 @@ jobs:
override: true
profile: minimal
- - name: Install flutter
- id: flutter
+ - name: Install Flutter
uses: subosito/flutter-action@v2
with:
channel: "stable"
@@ -59,13 +73,13 @@ jobs:
- uses: Swatinem/rust-cache@v2
with:
- prefix-key: ${{ matrix.os }}
+ prefix-key: macos-latest
workspaces: |
frontend/rust-lib
- uses: davidB/rust-cargo-make@v1
with:
- version: "0.36.6"
+ version: "0.37.15"
- name: Install prerequisites
working-directory: frontend
@@ -85,7 +99,7 @@ jobs:
- uses: futureware-tech/simulator-action@v3
id: simulator-action
with:
- model: 'iPhone 15'
+ model: "iPhone 15"
shutdown_after_job: false
# - name: Run AppFlowy on simulator
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 4ed6313dda7bd..63d04320610e4 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -479,6 +479,24 @@ jobs:
cache-from: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache
cache-to: type=registry,ref=${{ secrets.DOCKER_HUB_USERNAME }}/af_build_cache:buildcache,mode=max
+ notify-failure:
+ runs-on: ubuntu-latest
+ needs:
+ - build-for-macOS-x86_64
+ - build-for-windows
+ - build-for-linux
+ if: failure()
+ steps:
+ - uses: 8398a7/action-slack@v3
+ with:
+ status: ${{ job.status }}
+ text: |
+ 🔴🔴🔴Workflow ${{ github.workflow }} in repository ${{ github.repository }} was failed 🔴🔴🔴.
+ fields: repo,message,author,eventName,ref,workflow
+ env:
+ SLACK_WEBHOOK_URL: ${{ secrets.RELEASE_SLACK_WEBHOOK }}
+ if: always()
+
notify-discord:
runs-on: ubuntu-latest
needs:
diff --git a/.github/workflows/rust_ci.yaml b/.github/workflows/rust_ci.yaml
index f61925f9a6235..566aef3b7b281 100644
--- a/.github/workflows/rust_ci.yaml
+++ b/.github/workflows/rust_ci.yaml
@@ -8,6 +8,7 @@ on:
- "release/*"
paths:
- "frontend/rust-lib/**"
+ - ".github/workflows/rust_ci.yaml"
pull_request:
branches:
@@ -22,17 +23,74 @@ env:
RUST_TOOLCHAIN: "1.77.2"
jobs:
- test-on-ubuntu:
+ self-hosted-job:
+ if: github.event.pull_request.head.repo.full_name == github.repository
+ runs-on: self-hosted
+ steps:
+ - name: Checkout source code
+ uses: actions/checkout@v4
+
+ - name: Checkout Appflowy Cloud
+ uses: actions/checkout@v4
+ with:
+ repository: AppFlowy-IO/AppFlowy-Cloud
+ path: AppFlowy-Cloud
+
+ - name: Prepare Appflowy Cloud env
+ working-directory: AppFlowy-Cloud
+ run: |
+ cp deploy.env .env
+ sed -i '' 's|RUST_LOG=.*|RUST_LOG=trace|' .env
+ sed -i '' 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
+
+ - name: Ensure AppFlowy-Cloud is Running with Correct Version
+ working-directory: AppFlowy-Cloud
+ env:
+ APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
+ run: |
+ container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
+ if [ -z "$container_id" ]; then
+ echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
+ docker compose pull
+ docker compose up -d
+ echo "Waiting for the container to be ready..."
+ sleep 10
+ else
+ running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
+ if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
+ echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
+ docker compose pull
+ docker compose up -d
+ echo "Waiting for the container to be ready..."
+ sleep 10
+ else
+ echo "AppFlowy-Cloud is running with the correct version."
+ fi
+ fi
+
+ - name: Run rust-lib tests
+ working-directory: frontend/rust-lib
+ env:
+ RUST_LOG: info
+ RUST_BACKTRACE: 1
+ af_cloud_test_base_url: http://localhost
+ af_cloud_test_ws_url: ws://localhost/ws/v1
+ af_cloud_test_gotrue_url: http://localhost/gotrue
+ run: |
+ DISABLE_CI_TEST_LOG="true" cargo test --no-default-features --features="dart"
+
+ - name: rustfmt rust-lib
+ run: cargo fmt --all -- --check
+ working-directory: frontend/rust-lib/
+
+ - name: clippy rust-lib
+ run: cargo clippy --all-targets -- -D warnings
+ working-directory: frontend/rust-lib
+
+ ubuntu-job:
+ if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
steps:
- # - name: Maximize build space
- # uses: easimon/maximize-build-space@master
- # with:
- # root-reserve-mb: 2048
- # swap-size-mb: 1024
- # remove-dotnet: 'true'
-
- # the following step is required to avoid running out of space
- name: Maximize build space
run: |
sudo rm -rf /usr/share/dotnet
@@ -45,23 +103,16 @@ jobs:
uses: actions/checkout@v4
- name: Install Rust toolchain
- id: rust_toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
override: true
components: rustfmt, clippy
profile: minimal
-
- - name: Install prerequisites
- working-directory: frontend
- run: |
- cargo install --force cargo-make
- cargo install --force duckscript_cli
-
- uses: Swatinem/rust-cache@v2
with:
- prefix-key: "ubuntu-latest"
+ prefix-key: ${{ runner.os }}
+ cache-on-failure: true
workspaces: |
frontend/rust-lib
@@ -74,18 +125,34 @@ jobs:
- name: Prepare appflowy cloud env
working-directory: AppFlowy-Cloud
run: |
- # log level
cp deploy.env .env
sed -i 's|RUST_LOG=.*|RUST_LOG=trace|' .env
sed -i 's|API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://localhost|' .env
- - name: Run Docker-Compose
+ - name: Ensure AppFlowy-Cloud is Running with Correct Version
working-directory: AppFlowy-Cloud
env:
- BACKEND_VERSION: 0.3.24-amd64
+ APPFLOWY_CLOUD_VERSION: 0.6.4-amd64
run: |
- docker pull appflowyinc/appflowy_cloud:latest
- docker compose up -d
+ container_id=$(docker ps --filter name=appflowy-cloud-appflowy_cloud-1 -q)
+ if [ -z "$container_id" ]; then
+ echo "AppFlowy-Cloud container is not running. Pulling and starting the container..."
+ docker compose pull
+ docker compose up -d
+ echo "Waiting for the container to be ready..."
+ sleep 10
+ else
+ running_image=$(docker inspect --format='{{index .Config.Image}}' "$container_id")
+ if [ "$running_image" != "appflowy-cloud:$APPFLOWY_CLOUD_VERSION" ]; then
+ echo "AppFlowy-Cloud is running with an incorrect version. Pulling the correct version..."
+ docker compose pull
+ docker compose up -d
+ echo "Waiting for the container to be ready..."
+ sleep 10
+ else
+ echo "AppFlowy-Cloud is running with the correct version."
+ fi
+ fi
- name: Run rust-lib tests
working-directory: frontend/rust-lib
@@ -106,6 +173,12 @@ jobs:
run: cargo clippy --all-targets -- -D warnings
working-directory: frontend/rust-lib
+ - name: "Debug: show Appflowy-Cloud container logs"
+ if: failure()
+ working-directory: AppFlowy-Cloud
+ run: |
+ docker compose logs appflowy_cloud
+
- name: Clean up Docker images
run: |
docker image prune -af
diff --git a/.github/workflows/tauri2_ci.yaml b/.github/workflows/tauri2_ci.yaml
index 28414f099788e..6bbb7928ee7ef 100644
--- a/.github/workflows/tauri2_ci.yaml
+++ b/.github/workflows/tauri2_ci.yaml
@@ -1,4 +1,5 @@
name: Tauri-CI
+
on:
pull_request:
paths:
@@ -11,28 +12,47 @@ env:
NODE_VERSION: "18.16.0"
PNPM_VERSION: "8.5.0"
RUST_TOOLCHAIN: "1.77.2"
+ CARGO_MAKE_VERSION: "0.36.6"
+ CI: true
concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
jobs:
- tauri-build:
- if: github.event.pull_request.draft != true
- strategy:
- fail-fast: false
- matrix:
- platform: [ ubuntu-20.04 ]
-
- runs-on: ${{ matrix.platform }}
+ # tauri-build-self-hosted:
+ # if: github.event.pull_request.head.repo.full_name == github.repository
+ # runs-on: self-hosted
+ #
+ # steps:
+ # - uses: actions/checkout@v4
+ # - name: install frontend dependencies
+ # working-directory: frontend/appflowy_web_app
+ # run: |
+ # mkdir dist
+ # pnpm install
+ # cd src-tauri && cargo build
+ #
+ # - name: test and lint
+ # working-directory: frontend/appflowy_web_app
+ # run: |
+ # pnpm run lint:tauri
+ #
+ # - uses: tauri-apps/tauri-action@v0
+ # env:
+ # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ # with:
+ # tauriScript: pnpm tauri
+ # projectPath: frontend/appflowy_web_app
+ # args: "--debug"
+
+ tauri-build-ubuntu:
+ #if: github.event.pull_request.head.repo.full_name != github.repository
+ runs-on: ubuntu-20.04
- env:
- CI: true
steps:
- uses: actions/checkout@v4
-
- - name: Maximize build space (ubuntu only)
- if: matrix.platform == 'ubuntu-20.04'
+ - name: Maximize build space
run: |
sudo rm -rf /usr/share/dotnet
sudo rm -rf /opt/ghc
@@ -61,36 +81,27 @@ jobs:
override: true
profile: minimal
- - name: Rust cache
- uses: swatinem/rust-cache@v2
- with:
- workspaces: "./frontend/appflowy_web_app/src-tauri -> target"
-
- name: Node_modules cache
uses: actions/cache@v2
with:
path: frontend/appflowy_web_app/node_modules
key: node-modules-${{ runner.os }}
- - name: install dependencies (windows only)
- if: matrix.platform == 'windows-latest'
- working-directory: frontend
- run: |
- cargo install --force duckscript_cli
- vcpkg integrate install
-
- - name: install dependencies (ubuntu only)
- if: matrix.platform == 'ubuntu-20.04'
+ - 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
- - name: install cargo-make
+ - uses: taiki-e/install-action@v2
+ with:
+ tool: cargo-make@${{ env.CARGO_MAKE_VERSION }}
+
+ - name: install tauri deps tools
working-directory: frontend
run: |
- cargo install --force cargo-make
cargo make appflowy-tauri-deps-tools
+ shell: bash
- name: install frontend dependencies
working-directory: frontend/appflowy_web_app
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 69dc1af975763..f86f554620dbc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,41 @@
# Release Notes
+## Version 0.6.8 - 22/08/2024
+### New Features
+- Optimized date picker and mention block.
+- Added the ability to open database row on mobile.
+- Added the ability to invite members to workspace on mobile.
+- Added support for Monochrome theme on Android.
+- Added AI Bubble button on homepage on mobile.
+- Settings, trash, members and help & support have been moved into the settings pop up menu.
+
+### Bug Fixes
+- Removed Wayland header from AppImage build
+- Fixed the issue where pasting web image on mobile failed.
+- Fixed the issue where the name of newly created card in board view didn't show up.
+
+## Version 0.6.7 - 13/08/2024
+### New Features
+- Redesigned the icon picker design on Desktop.
+- Redesigned the notification page on Mobile.
+
+### Bug Fixes
+- Enhance the toolbar tooltip functionality on Desktop.
+- Enhance the slash menu user experience on Desktop.
+- Fixed the issue where list style overrides occurred during text pasting.
+- Fixed the issue where linking multiple databases in the same document could cause random loss of focus.
+
+## Version 0.6.6 - 30/07/2024
+### New Features
+- Upgrade your workspace to a premium plan to unlock more features and storage.
+- Image galleries and drag-and-drop image support in documents.
+
+### Bug Fixes
+- Fix minor UI issues on Desktop and Mobile.
+
+## Version 0.6.5 - 24/07/2024
+### New Features
+- Publish a Database to the Web
+
## Version 0.6.4 - 16/07/2024
### New Features
- Enhanced the message style on the AI chat page.
diff --git a/doc/readme/desktop_guide_1.jpg b/doc/readme/desktop_guide_1.jpg
new file mode 100644
index 0000000000000..d264c816957b9
Binary files /dev/null and b/doc/readme/desktop_guide_1.jpg differ
diff --git a/doc/readme/desktop_guide_2.jpg b/doc/readme/desktop_guide_2.jpg
new file mode 100644
index 0000000000000..d9cdbe5fc1aa7
Binary files /dev/null and b/doc/readme/desktop_guide_2.jpg differ
diff --git a/doc/readme/getting_started_1.png b/doc/readme/getting_started_1.png
new file mode 100644
index 0000000000000..8c3c7658ffc40
Binary files /dev/null and b/doc/readme/getting_started_1.png differ
diff --git a/doc/readme/mobile_guide_1.png b/doc/readme/mobile_guide_1.png
new file mode 100644
index 0000000000000..744fdf29dc38f
Binary files /dev/null and b/doc/readme/mobile_guide_1.png differ
diff --git a/doc/readme/mobile_guide_2.png b/doc/readme/mobile_guide_2.png
new file mode 100644
index 0000000000000..d92c0295c6999
Binary files /dev/null and b/doc/readme/mobile_guide_2.png differ
diff --git a/doc/readme/mobile_guide_3.png b/doc/readme/mobile_guide_3.png
new file mode 100644
index 0000000000000..9e3cc52d92a73
Binary files /dev/null and b/doc/readme/mobile_guide_3.png differ
diff --git a/doc/readme/mobile_guide_4.png b/doc/readme/mobile_guide_4.png
new file mode 100644
index 0000000000000..b39e03c251d4f
Binary files /dev/null and b/doc/readme/mobile_guide_4.png differ
diff --git a/doc/readme/mobile_guide_5.png b/doc/readme/mobile_guide_5.png
new file mode 100644
index 0000000000000..9083b80bed63a
Binary files /dev/null and b/doc/readme/mobile_guide_5.png differ
diff --git a/frontend/Makefile.toml b/frontend/Makefile.toml
index c4670654802e6..bfcd639cb297e 100644
--- a/frontend/Makefile.toml
+++ b/frontend/Makefile.toml
@@ -26,7 +26,7 @@ CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
CARGO_MAKE_CRATE_FS_NAME = "dart_ffi"
CARGO_MAKE_CRATE_NAME = "dart-ffi"
LIB_NAME = "dart_ffi"
-APPFLOWY_VERSION = "0.6.4"
+APPFLOWY_VERSION = "0.6.9"
FLUTTER_DESKTOP_FEATURES = "dart"
PRODUCT_NAME = "AppFlowy"
MACOSX_DEPLOYMENT_TARGET = "11.0"
diff --git a/frontend/appflowy_flutter/android/app/build.gradle b/frontend/appflowy_flutter/android/app/build.gradle
index fe788cbb529e7..35dcadda87420 100644
--- a/frontend/appflowy_flutter/android/app/build.gradle
+++ b/frontend/appflowy_flutter/android/app/build.gradle
@@ -87,6 +87,13 @@ android {
path "src/main/CMakeLists.txt"
}
}
+
+ // only support arm64-v8a
+ defaultConfig {
+ ndk {
+ abiFilters "arm64-v8a"
+ }
+ }
}
flutter {
diff --git a/frontend/appflowy_flutter/android/app/src/main/ic_launcher-playstore.png b/frontend/appflowy_flutter/android/app/src/main/ic_launcher-playstore.png
new file mode 100644
index 0000000000000..c691e14bdc563
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/ic_launcher-playstore.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/drawable/launch_background.xml b/frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_background.xml
similarity index 100%
rename from frontend/appflowy_flutter/android/app/src/main/res/drawable/launch_background.xml
rename to frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_background.xml
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_foreground.xml b/frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_foreground.xml
new file mode 100644
index 0000000000000..c7ec6fdd6f5c7
--- /dev/null
+++ b/frontend/appflowy_flutter/android/app/src/main/res/drawable/launcher_foreground.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
new file mode 100644
index 0000000000000..ba42ab6878248
--- /dev/null
+++ b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000000000..036d09bc5fd52
--- /dev/null
+++ b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index b00c03fd17463..911ee844c765f 100644
Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000000..1b466c0eb266a
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000000000..56ea852799b4d
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000000000..f4d14c0d6000d
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index e76d95c5be0a4..fe7a94797a341 100644
Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000000..15fb3c4ddf8d5
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000000000..63fa775f58c4e
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000000000..fda3c7fa3e197
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index c5188d2de41c1..61e49810e883f 100644
Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000000..132a0e9ff077a
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000000000..f9e393537dc8a
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000000..8efe0ff281f5d
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 3cc1a254c9a3f..be4cf46069de6 100644
Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000000..95a312fbc55cf
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000000000..a63acece70328
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000000..727cb0c58a7f3
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index c8f21cf1b3a20..c9e8059fe3c58 100644
Binary files a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png
new file mode 100644
index 0000000000000..d5ce932756139
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png
new file mode 100644
index 0000000000000..ad1543e064b8c
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000000000..010733d23d4a7
Binary files /dev/null and b/frontend/appflowy_flutter/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/frontend/appflowy_flutter/android/app/src/main/res/values/ic_launcher_background.xml b/frontend/appflowy_flutter/android/app/src/main/res/values/ic_launcher_background.xml
new file mode 100644
index 0000000000000..c5d5899fdf0a1
--- /dev/null
+++ b/frontend/appflowy_flutter/android/app/src/main/res/values/ic_launcher_background.xml
@@ -0,0 +1,4 @@
+
+
+ #FFFFFF
+
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/assets/icons/icons.json b/frontend/appflowy_flutter/assets/icons/icons.json
new file mode 100644
index 0000000000000..4ad858c4141d8
--- /dev/null
+++ b/frontend/appflowy_flutter/assets/icons/icons.json
@@ -0,0 +1 @@
+{ "artificial_intelligence": [ { "name": "ai-chip-spark", "keywords": [ "chip", "processor", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-cloud-spark", "keywords": [ "cloud", "internet", "server", "network", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-edit-spark", "keywords": [ "change", "edit", "modify", "pencil", "write", "writing", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-email-generator-spark", "keywords": [ "mail", "envelope", "inbox", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-gaming-spark", "keywords": [ "remote", "control", "controller", "technology", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-generate-landscape-image-spark", "keywords": [ "picture", "photography", "photo", "image", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-generate-music-spark", "keywords": [ "music", "audio", "note", "entertainment", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-generate-portrait-image-spark", "keywords": [ "picture", "photography", "photo", "image", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-generate-variation-spark", "keywords": [ "module", "application", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-navigation-spark", "keywords": [ "map", "location", "direction", "travel", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-network-spark", "keywords": [ "globe", "internet", "world", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-prompt-spark", "keywords": [ "app", "code", "apps", "window", "website", "web", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-redo-spark", "keywords": [ "arrow", "refresh", "sync", "synchronize", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-science-spark", "keywords": [ "atom", "scientific", "experiment", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-settings-spark", "keywords": [ "cog", "gear", "settings", "machine", "artificial", "intelligence" ], "content": "\n" }, { "name": "ai-technology-spark", "keywords": [ "lightbulb", "idea", "bright", "lighting", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "ai-upscale-spark", "keywords": [ "magnifier", "zoom", "view", "find", "search", "ai" ], "content": "\n" }, { "name": "ai-vehicle-spark-1", "keywords": [ "car", "automated", "transportation", "artificial", "intelligence", "ai" ], "content": "\n" }, { "name": "artificial-intelligence-spark", "keywords": [ "brain", "thought", "ai", "automated", "ai" ], "content": "\n" } ], "computer_devices": [ { "name": "adobe", "keywords": [], "content": "\n" }, { "name": "alt", "keywords": [ "windows", "key", "alt", "pc", "keyboard" ], "content": "\n" }, { "name": "amazon", "keywords": [], "content": "\n" }, { "name": "android", "keywords": [ "android", "code", "apps", "bugdroid", "programming" ], "content": "\n" }, { "name": "app-store", "keywords": [], "content": "\n" }, { "name": "apple", "keywords": [ "os", "system", "apple" ], "content": "\n" }, { "name": "asterisk-1", "keywords": [ "asterisk", "star", "keyboard" ], "content": "\n" }, { "name": "battery-alert-1", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "power", "battery", "alert", "warning" ], "content": "\n" }, { "name": "battery-charging", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "power", "battery", "charging" ], "content": "\n" }, { "name": "battery-empty-1", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "empty", "power", "battery" ], "content": "\n" }, { "name": "battery-empty-2", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "empty", "power", "battery" ], "content": "\n" }, { "name": "battery-full-1", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "power", "battery", "full" ], "content": "\n" }, { "name": "battery-low-1", "keywords": [ "phone", "mobile", "charge", "device", "electricity", "power", "battery", "low" ], "content": "\n" }, { "name": "battery-medium-1", "keywords": [ "phone", "mobile", "charge", "medium", "device", "electricity", "power", "battery" ], "content": "\n" }, { "name": "bluetooth", "keywords": [ "bluetooth", "internet", "server", "network", "wireless", "connection" ], "content": "\n" }, { "name": "bluetooth-disabled", "keywords": [ "bluetooth", "internet", "server", "network", "wireless", "disabled", "off", "connection" ], "content": "\n" }, { "name": "bluetooth-searching", "keywords": [ "bluetooth", "internet", "server", "network", "wireless", "searching", "connecting", "connection" ], "content": "\n" }, { "name": "browser-wifi", "keywords": [ "wireless", "wifi", "internet", "server", "network", "browser", "connection" ], "content": "\n" }, { "name": "chrome", "keywords": [], "content": "\n" }, { "name": "command", "keywords": [ "mac", "command", "apple", "keyboard" ], "content": "\n" }, { "name": "computer-chip-1", "keywords": [ "computer", "device", "chip", "electronics", "cpu", "microprocessor" ], "content": "\n" }, { "name": "computer-chip-2", "keywords": [ "core", "microprocessor", "device", "electronics", "chip", "computer" ], "content": "\n" }, { "name": "computer-pc-desktop", "keywords": [ "screen", "desktop", "monitor", "device", "electronics", "display", "pc", "computer" ], "content": "\n" }, { "name": "controller", "keywords": [ "remote", "quadcopter", "drones", "flying", "drone", "control", "controller", "technology", "fly" ], "content": "\n" }, { "name": "controller-1", "keywords": [ "remote", "quadcopter", "drones", "flying", "drone", "control", "controller", "technology", "fly" ], "content": "\n" }, { "name": "controller-wireless", "keywords": [ "remote", "gaming", "drones", "drone", "control", "controller", "technology", "console" ], "content": "\n" }, { "name": "cursor-click", "keywords": [], "content": "\n" }, { "name": "cyborg", "keywords": [ "artificial", "robotics", "intelligence", "machine", "technology", "android" ], "content": "\n" }, { "name": "cyborg-2", "keywords": [ "artificial", "robotics", "intelligence", "machine", "technology", "android" ], "content": "\n" }, { "name": "database", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc" ], "content": "\n" }, { "name": "database-check", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "check", "approve" ], "content": "\n" }, { "name": "database-lock", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "password", "security", "protection", "lock", "secure" ], "content": "\n" }, { "name": "database-refresh", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "refresh" ], "content": "\n" }, { "name": "database-remove", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "remove", "delete", "cross" ], "content": "\n" }, { "name": "database-server-1", "keywords": [ "server", "network", "internet" ], "content": "\n" }, { "name": "database-server-2", "keywords": [ "server", "network", "internet" ], "content": "\n" }, { "name": "database-setting", "keywords": [ "raid", "storage", "code", "disk", "programming", "database", "array", "hard", "disc", "setting" ], "content": "\n" }, { "name": "database-subtract-2-raid-storage-code-disk-programming-database-array-hard-disc-minus", "keywords": [], "content": "\n" }, { "name": "delete-keyboard", "keywords": [], "content": "\n" }, { "name": "desktop-chat", "keywords": [ "bubble", "chat", "customer", "service", "conversation", "display", "device" ], "content": "\n" }, { "name": "desktop-check", "keywords": [ "success", "approve", "device", "display", "desktop", "computer" ], "content": "\n" }, { "name": "desktop-code", "keywords": [ "desktop", "device", "display", "computer", "code", "terminal", "html", "css", "programming", "system" ], "content": "\n" }, { "name": "desktop-delete", "keywords": [ "device", "remove", "display", "computer", "deny", "desktop", "fail", "failure", "cross" ], "content": "\n" }, { "name": "desktop-dollar", "keywords": [ "cash", "desktop", "display", "device", "notification", "computer", "money", "currency" ], "content": "\n" }, { "name": "desktop-emoji", "keywords": [ "device", "display", "desktop", "padlock", "smiley" ], "content": "\n" }, { "name": "desktop-favorite-star", "keywords": [ "desktop", "device", "display", "like", "favorite", "star" ], "content": "\n" }, { "name": "desktop-game", "keywords": [ "controller", "display", "device", "computer", "games", "leisure" ], "content": "\n" }, { "name": "desktop-help", "keywords": [ "device", "help", "information", "display", "desktop", "question", "info" ], "content": "\n" }, { "name": "device-database-encryption-1", "keywords": [], "content": "\n" }, { "name": "discord", "keywords": [], "content": "\n" }, { "name": "drone", "keywords": [ "artificial", "robotics", "intelligence", "machine", "technology", "android", "flying" ], "content": "\n" }, { "name": "dropbox", "keywords": [], "content": "\n" }, { "name": "eject", "keywords": [ "eject", "unmount", "dismount", "remove", "keyboard" ], "content": "\n" }, { "name": "electric-cord-1", "keywords": [ "electricity", "electronic", "appliances", "device", "cord", "cable", "plug", "connection" ], "content": "\n" }, { "name": "electric-cord-3", "keywords": [ "electricity", "electronic", "appliances", "device", "cord", "cable", "plug", "connection" ], "content": "\n" }, { "name": "facebook-1", "keywords": [ "media", "facebook", "social" ], "content": "\n" }, { "name": "figma", "keywords": [], "content": "\n" }, { "name": "floppy-disk", "keywords": [ "disk", "floppy", "electronics", "device", "disc", "computer", "storage" ], "content": "\n" }, { "name": "gmail", "keywords": [], "content": "\n" }, { "name": "google", "keywords": [ "media", "google", "social" ], "content": "\n" }, { "name": "google-drive", "keywords": [], "content": "\n" }, { "name": "hand-held", "keywords": [ "tablet", "kindle", "device", "electronics", "ipad", "computer" ], "content": "\n" }, { "name": "hand-held-tablet-drawing", "keywords": [ "tablet", "kindle", "device", "electronics", "ipad", "digital", "drawing", "canvas" ], "content": "\n" }, { "name": "hand-held-tablet-writing", "keywords": [ "tablet", "kindle", "device", "electronics", "ipad", "writing", "digital", "paper", "notepad" ], "content": "\n" }, { "name": "hard-disk", "keywords": [ "device", "disc", "drive", "disk", "electronics", "platter", "turntable", "raid", "storage" ], "content": "\n" }, { "name": "hard-drive-1", "keywords": [ "disk", "device", "electronics", "disc", "drive", "raid", "storage" ], "content": "\n" }, { "name": "instagram", "keywords": [], "content": "\n" }, { "name": "keyboard", "keywords": [ "keyboard", "device", "electronics", "dvorak", "qwerty" ], "content": "\n" }, { "name": "keyboard-virtual", "keywords": [ "remote", "device", "electronics", "qwerty", "keyboard", "virtual", "interface" ], "content": "\n" }, { "name": "keyboard-wireless-2", "keywords": [ "remote", "device", "wireless", "electronics", "qwerty", "keyboard", "bluetooth" ], "content": "\n" }, { "name": "laptop-charging", "keywords": [ "device", "laptop", "electronics", "computer", "notebook", "charging" ], "content": "\n" }, { "name": "linkedin", "keywords": [ "network", "linkedin", "professional" ], "content": "\n" }, { "name": "local-storage-folder", "keywords": [], "content": "\n" }, { "name": "meta", "keywords": [], "content": "\n" }, { "name": "mouse", "keywords": [ "device", "electronics", "mouse" ], "content": "\n" }, { "name": "mouse-wireless", "keywords": [ "remote", "wireless", "device", "electronics", "mouse", "computer" ], "content": "\n" }, { "name": "mouse-wireless-1", "keywords": [ "remote", "wireless", "device", "electronics", "mouse", "computer" ], "content": "\n" }, { "name": "netflix", "keywords": [], "content": "\n" }, { "name": "network", "keywords": [ "network", "server", "internet", "ethernet", "connection" ], "content": "\n" }, { "name": "next", "keywords": [ "next", "arrow", "right", "keyboard" ], "content": "\n" }, { "name": "paypal", "keywords": [ "payment", "paypal" ], "content": "\n" }, { "name": "play-store", "keywords": [], "content": "\n" }, { "name": "printer", "keywords": [ "scan", "device", "electronics", "printer", "print", "computer" ], "content": "\n" }, { "name": "return-2", "keywords": [ "arrow", "return", "enter", "keyboard" ], "content": "\n" }, { "name": "screen-1", "keywords": [ "screen", "device", "electronics", "monitor", "diplay", "computer" ], "content": "\n" }, { "name": "screen-2", "keywords": [ "screen", "device", "electronics", "monitor", "diplay", "computer" ], "content": "\n" }, { "name": "screen-curve", "keywords": [ "screen", "curved", "device", "electronics", "monitor", "diplay", "computer" ], "content": "\n" }, { "name": "screensaver-monitor-wallpaper", "keywords": [], "content": "\n" }, { "name": "shift", "keywords": [ "key", "shift", "up", "arrow", "keyboard" ], "content": "\n" }, { "name": "shredder", "keywords": [ "device", "electronics", "shred", "paper", "cut", "destroy", "remove", "delete" ], "content": "\n" }, { "name": "signal-loading", "keywords": [ "bracket", "loading", "internet", "angle", "signal", "server", "network", "connecting", "connection" ], "content": "\n" }, { "name": "slack", "keywords": [], "content": "\n" }, { "name": "spotify", "keywords": [], "content": "\n" }, { "name": "telegram", "keywords": [], "content": "\n" }, { "name": "tiktok", "keywords": [], "content": "\n" }, { "name": "tinder", "keywords": [], "content": "\n" }, { "name": "twitter", "keywords": [ "media", "twitter", "social" ], "content": "\n" }, { "name": "usb-drive", "keywords": [ "usb", "drive", "stick", "memory", "storage", "data", "connection" ], "content": "\n" }, { "name": "virtual-reality", "keywords": [ "gaming", "virtual", "gear", "controller", "reality", "games", "headset", "technology", "vr", "eyewear" ], "content": "\n" }, { "name": "voice-mail", "keywords": [ "mic", "audio", "mike", "music", "microphone" ], "content": "\n" }, { "name": "voice-mail-off", "keywords": [ "mic", "audio", "mike", "music", "microphone", "mute", "off" ], "content": "\n" }, { "name": "VPN-connection", "keywords": [], "content": "\n" }, { "name": "watch-1", "keywords": [ "device", "timepiece", "cirle", "electronics", "face", "blank", "watch", "smart" ], "content": "\n" }, { "name": "watch-2", "keywords": [ "device", "square", "timepiece", "electronics", "face", "blank", "watch", "smart" ], "content": "\n" }, { "name": "watch-circle-charging", "keywords": [ "device", "timepiece", "circle", "watch", "round", "charge", "charging", "power" ], "content": "\n" }, { "name": "watch-circle-heartbeat-monitor-1", "keywords": [ "device", "timepiece", "circle", "watch", "round", "heart", "beat", "monitor", "healthcare" ], "content": "\n" }, { "name": "watch-circle-heartbeat-monitor-2", "keywords": [ "device", "timepiece", "circle", "watch", "round", "heart", "beat", "monitor", "healthcare" ], "content": "\n" }, { "name": "watch-circle-menu", "keywords": [ "device", "timepiece", "circle", "watch", "round", "menu", "list", "option", "app" ], "content": "\n" }, { "name": "watch-circle-time", "keywords": [ "device", "timepiece", "circle", "watch", "round", "time", "clock", "analog" ], "content": "\n" }, { "name": "webcam", "keywords": [ "webcam", "camera", "future", "tech", "chat", "skype", "technology", "video" ], "content": "\n" }, { "name": "webcam-video", "keywords": [ "work", "video", "meeting", "camera", "company", "conference", "office" ], "content": "\n" }, { "name": "webcam-video-circle", "keywords": [ "work", "video", "meeting", "camera", "company", "conference", "office" ], "content": "\n" }, { "name": "webcam-video-off", "keywords": [ "work", "video", "meeting", "camera", "company", "conference", "office", "off" ], "content": "\n" }, { "name": "whatsapp", "keywords": [], "content": "\n" }, { "name": "wifi", "keywords": [ "wireless", "wifi", "internet", "server", "network", "connection" ], "content": "\n" }, { "name": "wifi-antenna", "keywords": [ "wireless", "wifi", "internet", "server", "network", "antenna", "connection" ], "content": "\n" }, { "name": "wifi-disabled", "keywords": [ "wireless", "wifi", "internet", "server", "network", "disabled", "off", "offline", "connection" ], "content": "\n" }, { "name": "wifi-horizontal", "keywords": [ "wireless", "wifi", "internet", "server", "network", "horizontal", "connection" ], "content": "\n" }, { "name": "wifi-router", "keywords": [ "wireless", "wifi", "internet", "server", "network", "connection" ], "content": "\n" }, { "name": "windows", "keywords": [ "os", "system", "microsoft" ], "content": "\n" } ], "culture": [ { "name": "christian-cross-1", "keywords": [ "religion", "christian", "cross", "culture", "bold" ], "content": "\n" }, { "name": "christian-cross-2", "keywords": [ "religion", "christian", "cross", "culture", "bold" ], "content": "\n" }, { "name": "christianity", "keywords": [ "religion", "jesus", "christianity", "christ", "fish", "culture" ], "content": "\n" }, { "name": "dhammajak", "keywords": [ "religion", "dhammajak", "culture", "bhuddhism", "buddish" ], "content": "\n" }, { "name": "hexagram", "keywords": [ "star", "jew", "jewish", "judaism", "hexagram", "culture", "religion", "david" ], "content": "\n" }, { "name": "hinduism", "keywords": [ "religion", "hinduism", "culture", "hindu" ], "content": "\n" }, { "name": "islam", "keywords": [ "religion", "islam", "moon", "crescent", "muslim", "culture", "star" ], "content": "\n" }, { "name": "news-paper", "keywords": [ "newspaper", "periodical", "fold", "content", "entertainment" ], "content": "\n" }, { "name": "peace-symbol", "keywords": [ "religion", "peace", "war", "culture", "symbol" ], "content": "\n" }, { "name": "politics-compaign", "keywords": [], "content": "\n" }, { "name": "politics-speech", "keywords": [], "content": "\n" }, { "name": "politics-vote-2", "keywords": [], "content": "\n" }, { "name": "ticket-1", "keywords": [ "hobby", "ticket", "event", "entertainment", "stub", "theater", "entertainment", "culture" ], "content": "\n" }, { "name": "tickets", "keywords": [ "hobby", "ticket", "event", "entertainment", "stub", "theater", "entertainment", "culture" ], "content": "\n" }, { "name": "yin-yang-symbol", "keywords": [ "religion", "tao", "yin", "yang", "taoism", "culture", "symbol" ], "content": "\n" }, { "name": "zodiac-1", "keywords": [ "sign", "astrology", "stars", "space", "scorpio" ], "content": "\n" }, { "name": "zodiac-10", "keywords": [ "sign", "astrology", "stars", "space", "pisces" ], "content": "\n" }, { "name": "zodiac-11", "keywords": [ "sign", "astrology", "stars", "space", "sagittarius" ], "content": "\n" }, { "name": "zodiac-12", "keywords": [ "sign", "astrology", "stars", "space", "cancer" ], "content": "\n" }, { "name": "zodiac-2", "keywords": [ "sign", "astrology", "stars", "space", "virgo" ], "content": "\n" }, { "name": "zodiac-3", "keywords": [ "sign", "astrology", "stars", "space", "leo" ], "content": "\n" }, { "name": "zodiac-4", "keywords": [ "sign", "astrology", "stars", "space", "aquarius" ], "content": "\n" }, { "name": "zodiac-5", "keywords": [ "sign", "astrology", "stars", "space", "taurus" ], "content": "\n" }, { "name": "zodiac-6", "keywords": [ "sign", "astrology", "stars", "space", "capricorn" ], "content": "\n" }, { "name": "zodiac-7", "keywords": [ "sign", "astrology", "stars", "space", "ares" ], "content": "\n" }, { "name": "zodiac-8", "keywords": [ "sign", "astrology", "stars", "space", "libra" ], "content": "\n" }, { "name": "zodiac-9", "keywords": [ "sign", "astrology", "stars", "space", "gemini" ], "content": "\n" } ], "entertainment": [ { "name": "balloon", "keywords": [ "hobby", "entertainment", "party", "balloon" ], "content": "\n" }, { "name": "bow", "keywords": [ "entertainment", "gaming", "bow", "weapon" ], "content": "\n" }, { "name": "button-fast-forward-1", "keywords": [ "button", "controls", "fast", "forward", "movies", "television", "video", "tv" ], "content": "\n" }, { "name": "button-fast-forward-2", "keywords": [ "button", "controls", "fast", "forward", "movies", "television", "video", "tv" ], "content": "\n" }, { "name": "button-next", "keywords": [ "button", "television", "buttons", "movies", "skip", "next", "video", "controls" ], "content": "\n" }, { "name": "button-pause-2", "keywords": [ "button", "television", "buttons", "movies", "tv", "pause", "video", "controls" ], "content": "\n" }, { "name": "button-play", "keywords": [ "button", "television", "buttons", "movies", "play", "tv", "video", "controls" ], "content": "\n" }, { "name": "button-power-1", "keywords": [ "power", "button", "on", "off" ], "content": "\n" }, { "name": "button-previous", "keywords": [ "button", "television", "buttons", "movies", "skip", "previous", "video", "controls" ], "content": "\n" }, { "name": "button-record-3", "keywords": [ "button", "television", "buttons", "movies", "record", "tv", "video", "controls" ], "content": "\n" }, { "name": "button-rewind-1", "keywords": [ "rewind", "television", "button", "movies", "buttons", "tv", "video", "controls" ], "content": "\n" }, { "name": "button-rewind-2", "keywords": [ "rewind", "television", "button", "movies", "buttons", "tv", "video", "controls" ], "content": "\n" }, { "name": "button-stop", "keywords": [ "button", "television", "buttons", "movies", "stop", "tv", "video", "controls" ], "content": "\n" }, { "name": "camera-video", "keywords": [ "film", "television", "tv", "camera", "movies", "video", "recorder" ], "content": "\n" }, { "name": "cards", "keywords": [], "content": "\n" }, { "name": "chess-bishop", "keywords": [], "content": "\n" }, { "name": "chess-king", "keywords": [], "content": "\n" }, { "name": "chess-knight", "keywords": [], "content": "\n" }, { "name": "chess-pawn", "keywords": [], "content": "\n" }, { "name": "cloud-gaming-1", "keywords": [ "entertainment", "cloud", "gaming" ], "content": "\n" }, { "name": "clubs-symbol", "keywords": [ "entertainment", "gaming", "card", "clubs", "symbol" ], "content": "\n" }, { "name": "diamonds-symbol", "keywords": [ "entertainment", "gaming", "card", "diamonds", "symbol" ], "content": "\n" }, { "name": "dice-1", "keywords": [], "content": "\n" }, { "name": "dice-2", "keywords": [], "content": "\n" }, { "name": "dice-3", "keywords": [], "content": "\n" }, { "name": "dice-4", "keywords": [], "content": "\n" }, { "name": "dice-5", "keywords": [], "content": "\n" }, { "name": "dice-6", "keywords": [], "content": "\n" }, { "name": "dices-entertainment-gaming-dices", "keywords": [], "content": "\n" }, { "name": "earpods", "keywords": [ "airpods", "audio", "earpods", "music", "earbuds", "true", "wireless", "entertainment" ], "content": "\n" }, { "name": "epic-games-1", "keywords": [ "epic", "games", "entertainment", "gaming" ], "content": "\n" }, { "name": "esports", "keywords": [ "entertainment", "gaming", "esports" ], "content": "\n" }, { "name": "fireworks-rocket", "keywords": [ "hobby", "entertainment", "party", "fireworks", "rocket" ], "content": "\n" }, { "name": "gameboy", "keywords": [ "entertainment", "gaming", "device", "gameboy" ], "content": "\n" }, { "name": "gramophone", "keywords": [ "music", "audio", "note", "gramophone", "player", "vintage", "entertainment" ], "content": "\n" }, { "name": "hearts-symbol", "keywords": [ "entertainment", "gaming", "card", "hearts", "symbol" ], "content": "\n" }, { "name": "music-equalizer", "keywords": [ "music", "audio", "note", "wave", "sound", "equalizer", "entertainment" ], "content": "\n" }, { "name": "music-note-1", "keywords": [ "music", "audio", "note", "entertainment" ], "content": "\n" }, { "name": "music-note-2", "keywords": [ "music", "audio", "note", "entertainment" ], "content": "\n" }, { "name": "music-note-off-1", "keywords": [ "music", "audio", "note", "off", "mute", "entertainment" ], "content": "\n" }, { "name": "music-note-off-2", "keywords": [ "music", "audio", "note", "off", "mute", "entertainment" ], "content": "\n" }, { "name": "nintendo-switch", "keywords": [ "nintendo", "switch", "entertainment", "gaming" ], "content": "\n" }, { "name": "one-vesus-one", "keywords": [ "entertainment", "gaming", "one", "vesus", "one" ], "content": "\n" }, { "name": "pacman", "keywords": [ "entertainment", "gaming", "pacman", "video" ], "content": "\n" }, { "name": "party-popper", "keywords": [ "hobby", "entertainment", "party", "popper", "confetti", "event" ], "content": "\n" }, { "name": "play-list-4", "keywords": [ "screen", "television", "display", "player", "movies", "players", "tv", "media", "video", "entertainment" ], "content": "\n" }, { "name": "play-list-5", "keywords": [ "player", "television", "movies", "slider", "media", "tv", "players", "video", "stack", "entertainment" ], "content": "\n" }, { "name": "play-list-8", "keywords": [ "player", "television", "movies", "slider", "media", "tv", "players", "video", "stack", "entertainment" ], "content": "\n" }, { "name": "play-list-9", "keywords": [ "player", "television", "movies", "slider", "media", "tv", "players", "video", "stack", "entertainment" ], "content": "\n" }, { "name": "play-list-folder", "keywords": [ "player", "television", "movies", "slider", "media", "tv", "players", "video" ], "content": "\n" }, { "name": "play-station", "keywords": [ "play", "station", "entertainment", "gaming" ], "content": "\n" }, { "name": "radio", "keywords": [ "antenna", "audio", "music", "radio", "entertainment" ], "content": "\n" }, { "name": "recording-tape-bubble-circle", "keywords": [ "phone", "device", "mail", "mobile", "voice", "machine", "answering", "chat", "entertainment" ], "content": "\n" }, { "name": "recording-tape-bubble-square", "keywords": [ "phone", "device", "mail", "mobile", "voice", "machine", "answering", "chat", "entertainment" ], "content": "\n" }, { "name": "song-recommendation", "keywords": [ "song", "recommendation", "entertainment" ], "content": "\n" }, { "name": "spades-symbol", "keywords": [ "entertainment", "gaming", "card", "spades", "symbol" ], "content": "\n" }, { "name": "speaker-1", "keywords": [ "speaker", "music", "audio", "subwoofer", "entertainment" ], "content": "\n" }, { "name": "speaker-2", "keywords": [ "speakers", "music", "audio", "entertainment" ], "content": "\n" }, { "name": "stream", "keywords": [ "stream", "entertainment", "gaming" ], "content": "\n" }, { "name": "tape-cassette-record", "keywords": [ "music", "entertainment", "tape", "cassette", "record" ], "content": "\n" }, { "name": "volume-down", "keywords": [ "speaker", "down", "volume", "control", "audio", "music", "decrease", "entertainment" ], "content": "\n" }, { "name": "volume-level-high", "keywords": [ "speaker", "high", "volume", "control", "audio", "music", "entertainment" ], "content": "\n" }, { "name": "volume-level-low", "keywords": [ "volume", "speaker", "lower", "down", "control", "music", "low", "audio", "entertainment" ], "content": "\n" }, { "name": "volume-level-off", "keywords": [ "volume", "speaker", "control", "music", "audio", "entertainment" ], "content": "\n" }, { "name": "volume-mute", "keywords": [ "speaker", "remove", "volume", "control", "audio", "music", "mute", "off", "cross", "entertainment" ], "content": "\n" }, { "name": "volume-off", "keywords": [ "speaker", "music", "mute", "volume", "control", "audio", "off", "mute", "entertainment" ], "content": "\n" }, { "name": "vr-headset-1", "keywords": [ "entertainment", "gaming", "vr", "headset" ], "content": "\n" }, { "name": "vr-headset-2", "keywords": [ "entertainment", "gaming", "vr", "headset" ], "content": "\n" }, { "name": "xbox", "keywords": [ "xbox", "entertainment", "gaming" ], "content": "\n" } ], "food_drink": [ { "name": "beer-mug", "keywords": [ "beer", "cook", "brewery", "drink", "mug", "cooking", "nutrition", "brew", "brewing", "food" ], "content": "\n" }, { "name": "beer-pitch", "keywords": [ "drink", "glass", "beer", "pitch" ], "content": "\n" }, { "name": "burger", "keywords": [ "burger", "fast", "cook", "cooking", "nutrition", "food" ], "content": "\n" }, { "name": "burrito-fastfood", "keywords": [], "content": "\n" }, { "name": "cake-slice", "keywords": [ "cherry", "cake", "birthday", "event", "special", "sweet", "bake" ], "content": "\n" }, { "name": "candy-cane", "keywords": [ "candy", "sweet", "cane", "christmas" ], "content": "\n" }, { "name": "champagne-party-alcohol", "keywords": [], "content": "\n" }, { "name": "cheese", "keywords": [ "cook", "cheese", "animal", "products", "cooking", "nutrition", "dairy", "food" ], "content": "\n" }, { "name": "cherries", "keywords": [ "cook", "plant", "cherry", "plants", "cooking", "nutrition", "vegetarian", "fruit", "food", "cherries" ], "content": "\n" }, { "name": "chicken-grilled-stream", "keywords": [], "content": "\n" }, { "name": "cocktail", "keywords": [ "cook", "alcohol", "food", "cocktail", "drink", "cooking", "nutrition", "alcoholic", "beverage", "glass" ], "content": "\n" }, { "name": "coffee-bean", "keywords": [ "cook", "cooking", "nutrition", "coffee", "bean" ], "content": "\n" }, { "name": "coffee-mug", "keywords": [ "coffee", "cook", "cup", "drink", "mug", "cooking", "nutrition", "cafe", "caffeine", "food" ], "content": "\n" }, { "name": "coffee-takeaway-cup", "keywords": [ "cup", "coffee", "hot", "takeaway", "drink", "caffeine" ], "content": "\n" }, { "name": "donut", "keywords": [ "dessert", "donut" ], "content": "\n" }, { "name": "fork-knife", "keywords": [ "fork", "spoon", "knife", "food", "dine", "cook", "utensils", "eat", "restaurant", "dining", "kitchenware" ], "content": "\n" }, { "name": "fork-spoon", "keywords": [ "fork", "spoon", "food", "dine", "cook", "utensils", "eat", "restaurant", "dining", "kitchenware" ], "content": "\n" }, { "name": "ice-cream-2", "keywords": [ "cook", "frozen", "popsicle", "freezer", "nutrition", "cream", "stick", "cold", "ice", "cooking" ], "content": "\n" }, { "name": "ice-cream-3", "keywords": [ "cook", "frozen", "cone", "cream", "ice", "cooking", "nutrition", "freezer", "cold", "food" ], "content": "\n" }, { "name": "lemon-fruit-seasoning", "keywords": [], "content": "\n" }, { "name": "microwave", "keywords": [ "cook", "food", "appliances", "cooking", "nutrition", "appliance", "microwave", "kitchenware" ], "content": "\n" }, { "name": "milkshake", "keywords": [ "milkshake", "drink", "takeaway", "cup", "cold", "beverage" ], "content": "\n" }, { "name": "popcorn", "keywords": [ "cook", "corn", "movie", "snack", "cooking", "nutrition", "bake", "popcorn" ], "content": "\n" }, { "name": "pork-meat", "keywords": [], "content": "\n" }, { "name": "refrigerator", "keywords": [ "fridge", "cook", "appliances", "cooking", "nutrition", "freezer", "appliance", "food", "kitchenware" ], "content": "\n" }, { "name": "serving-dome", "keywords": [ "cook", "tool", "dome", "kitchen", "serving", "paltter", "dish", "tools", "food", "kitchenware" ], "content": "\n" }, { "name": "shrimp", "keywords": [ "sea", "food", "shrimp" ], "content": "\n" }, { "name": "strawberry", "keywords": [ "fruit", "sweet", "berries", "plant", "strawberry" ], "content": "\n" }, { "name": "tea-cup", "keywords": [ "herbal", "cook", "tea", "tisane", "cup", "drink", "cooking", "nutrition", "mug", "food" ], "content": "\n" }, { "name": "toast", "keywords": [ "bread", "toast", "breakfast" ], "content": "\n" }, { "name": "water-glass", "keywords": [ "glass", "water", "juice", "drink", "liquid" ], "content": "\n" }, { "name": "wine", "keywords": [ "drink", "cook", "glass", "cooking", "wine", "nutrition", "food" ], "content": "\n" } ], "health": [ { "name": "ambulance", "keywords": [ "car", "emergency", "health", "medical", "ambulance" ], "content": "\n" }, { "name": "bacteria-virus-cells-biology", "keywords": [], "content": "\n" }, { "name": "bandage", "keywords": [ "health", "medical", "hospital", "medicine", "capsule", "bandage", "vaccine" ], "content": "\n" }, { "name": "blood-bag-donation", "keywords": [], "content": "\n" }, { "name": "blood-donate-drop", "keywords": [], "content": "\n" }, { "name": "blood-drop-donation", "keywords": [], "content": "\n" }, { "name": "brain", "keywords": [ "medical", "health", "brain" ], "content": "\n" }, { "name": "brain-cognitive", "keywords": [ "health", "medical", "brain", "cognitive", "specialities" ], "content": "\n" }, { "name": "call-center-support-service", "keywords": [], "content": "\n" }, { "name": "checkup-medical-report-clipboard", "keywords": [], "content": "\n" }, { "name": "ear-hearing", "keywords": [ "health", "medical", "hearing", "ear" ], "content": "\n" }, { "name": "eye-optic", "keywords": [ "health", "medical", "eye", "optic" ], "content": "\n" }, { "name": "flu-mask", "keywords": [ "health", "medical", "hospital", "mask", "flu", "vaccine", "protection" ], "content": "\n" }, { "name": "health-care-2", "keywords": [ "health", "medical", "hospital", "heart", "care", "symbol" ], "content": "\n" }, { "name": "heart-rate-pulse-graph", "keywords": [], "content": "\n" }, { "name": "heart-rate-search", "keywords": [ "health", "medical", "monitor", "heart", "rate", "search" ], "content": "\n" }, { "name": "hospital-sign-circle", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "circle", "emergency" ], "content": "\n" }, { "name": "hospital-sign-square", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "square", "emergency" ], "content": "\n" }, { "name": "insurance-hand", "keywords": [ "health", "medical", "insurance", "hand", "cross" ], "content": "\n" }, { "name": "medical-bag", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "bag", "medicine", "medkit" ], "content": "\n" }, { "name": "medical-cross-sign-healthcare", "keywords": [], "content": "\n" }, { "name": "medical-cross-symbol", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "emergency" ], "content": "\n" }, { "name": "medical-files-report-history", "keywords": [], "content": "\n" }, { "name": "medical-ribbon-1", "keywords": [ "ribbon", "medical", "cancer", "health", "beauty", "symbol" ], "content": "\n" }, { "name": "medical-search-diagnosis", "keywords": [], "content": "\n" }, { "name": "microscope-observation-sciene", "keywords": [], "content": "\n" }, { "name": "nurse-assistant-emergency", "keywords": [], "content": "\n" }, { "name": "nurse-hat", "keywords": [ "health", "medical", "hospital", "nurse", "doctor", "cap" ], "content": "\n" }, { "name": "online-medical-call-service", "keywords": [], "content": "\n" }, { "name": "online-medical-service-monitor", "keywords": [], "content": "\n" }, { "name": "online-medical-web-service", "keywords": [], "content": "\n" }, { "name": "petri-dish-lab-equipment", "keywords": [], "content": "\n" }, { "name": "pharmacy", "keywords": [ "health", "medical", "pharmacy", "sign", "medicine", "mortar", "pestle" ], "content": "\n" }, { "name": "prescription-pills-drugs-healthcare", "keywords": [], "content": "\n" }, { "name": "sign-cross-square", "keywords": [ "health", "sign", "medical", "symbol", "hospital", "cross", "square" ], "content": "\n" }, { "name": "sos-help-emergency-sign", "keywords": [], "content": "\n" }, { "name": "stethoscope", "keywords": [ "instrument", "health", "medical", "stethoscope" ], "content": "\n" }, { "name": "syringe", "keywords": [ "instrument", "medical", "syringe", "health", "beauty", "needle" ], "content": "\n" }, { "name": "tablet-capsule", "keywords": [ "health", "medical", "hospital", "medicine", "capsule", "tablet" ], "content": "\n" }, { "name": "tooth", "keywords": [ "health", "medical", "tooth" ], "content": "\n" }, { "name": "virus-antivirus", "keywords": [ "health", "medical", "covid19", "flu", "influenza", "virus", "antivirus" ], "content": "\n" }, { "name": "waiting-appointments-calendar", "keywords": [], "content": "\n" }, { "name": "wheelchair", "keywords": [ "health", "medical", "hospital", "wheelchair", "disable", "help", "sign" ], "content": "\n" } ], "images_photography": [ { "name": "auto-flash", "keywords": [], "content": "\n" }, { "name": "camera-1", "keywords": [ "photos", "picture", "camera", "photography", "photo", "pictures" ], "content": "\n" }, { "name": "camera-disabled", "keywords": [ "photos", "picture", "camera", "photography", "photo", "pictures", "disabled", "off" ], "content": "\n" }, { "name": "camera-loading", "keywords": [ "photos", "picture", "camera", "photography", "photo", "pictures", "loading", "option", "setting" ], "content": "\n" }, { "name": "camera-square", "keywords": [ "photos", "picture", "camera", "photography", "photo", "pictures", "frame", "square" ], "content": "\n" }, { "name": "composition-oval", "keywords": [ "camera", "frame", "composition", "photography", "pictures", "landscape", "photo", "oval" ], "content": "\n" }, { "name": "composition-vertical", "keywords": [ "camera", "portrait", "frame", "vertical", "composition", "photography", "photo" ], "content": "\n" }, { "name": "compsition-horizontal", "keywords": [ "camera", "horizontal", "panorama", "composition", "photography", "photo", "pictures" ], "content": "\n" }, { "name": "edit-image-photo", "keywords": [], "content": "\n" }, { "name": "film-roll-1", "keywords": [ "photos", "camera", "shutter", "picture", "photography", "pictures", "photo", "film", "roll" ], "content": "\n" }, { "name": "film-slate", "keywords": [ "pictures", "photo", "film", "slate" ], "content": "\n" }, { "name": "flash-1", "keywords": [ "flash", "power", "connect", "charge", "electricity", "lightning" ], "content": "\n" }, { "name": "flash-2", "keywords": [ "flash", "power", "connect", "charge", "electricity", "lightning" ], "content": "\n" }, { "name": "flash-3", "keywords": [ "flash", "power", "connect", "charge", "electricity", "lightning" ], "content": "\n" }, { "name": "flash-off", "keywords": [ "flash", "power", "connect", "charge", "off", "electricity", "lightning" ], "content": "\n" }, { "name": "flower", "keywords": [ "photos", "photo", "picture", "camera", "photography", "pictures", "flower", "image" ], "content": "\n" }, { "name": "focus-points", "keywords": [ "camera", "frame", "photography", "pictures", "photo", "focus", "position" ], "content": "\n" }, { "name": "landscape-2", "keywords": [ "photos", "photo", "landscape", "picture", "photography", "camera", "pictures", "image" ], "content": "\n" }, { "name": "landscape-setting", "keywords": [ "design", "composition", "horizontal", "lanscape" ], "content": "\n" }, { "name": "laptop-camera", "keywords": [ "photos", "photo", "picture", "photography", "camera", "pictures", "laptop", "computer" ], "content": "\n" }, { "name": "mobile-phone-camera", "keywords": [ "photos", "photo", "picture", "photography", "camera", "pictures", "phone" ], "content": "\n" }, { "name": "orientation-landscape", "keywords": [ "photos", "photo", "orientation", "landscape", "picture", "photography", "camera", "pictures", "image" ], "content": "\n" }, { "name": "orientation-portrait", "keywords": [ "photos", "photo", "orientation", "portrait", "picture", "photography", "camera", "pictures", "image" ], "content": "\n" }, { "name": "polaroid-four", "keywords": [ "photos", "camera", "polaroid", "picture", "photography", "pictures", "four", "photo", "image" ], "content": "\n" } ], "interface_essential": [ { "name": "add-1", "keywords": [ "expand", "cross", "buttons", "button", "more", "remove", "plus", "add", "+", "mathematics", "math" ], "content": "\n" }, { "name": "add-bell-notification", "keywords": [ "notification", "alarm", "alert", "bell", "add" ], "content": "\n" }, { "name": "add-circle", "keywords": [ "button", "remove", "cross", "add", "buttons", "plus", "circle", "+", "mathematics", "math" ], "content": "\n" }, { "name": "add-layer-2", "keywords": [ "layer", "add", "design", "plus", "layers", "square", "box" ], "content": "\n" }, { "name": "add-square", "keywords": [ "square", "remove", "cross", "buttons", "add", "plus", "button", "+", "mathematics", "math" ], "content": "\n" }, { "name": "alarm-clock", "keywords": [ "time", "tock", "stopwatch", "measure", "clock", "tick" ], "content": "\n" }, { "name": "align-back-1", "keywords": [ "back", "design", "layer", "layers", "pile", "stack", "arrange", "square" ], "content": "\n" }, { "name": "align-center", "keywords": [ "text", "alignment", "align", "paragraph", "centered", "formatting", "center" ], "content": "\n" }, { "name": "align-front-1", "keywords": [ "design", "front", "layer", "layers", "pile", "stack", "arrange", "square" ], "content": "\n" }, { "name": "align-left", "keywords": [ "paragraph", "text", "alignment", "align", "left", "formatting", "right" ], "content": "\n" }, { "name": "align-right", "keywords": [ "rag", "paragraph", "text", "alignment", "align", "right", "formatting", "left" ], "content": "\n" }, { "name": "ampersand", "keywords": [], "content": "\n" }, { "name": "archive-box", "keywords": [ "box", "content", "banker", "archive", "file" ], "content": "\n" }, { "name": "arrow-bend-left-down-2", "keywords": [ "arrow", "bend", "curve", "change", "direction", "left", "to", "down" ], "content": "\n" }, { "name": "arrow-bend-right-down-2", "keywords": [ "arrow", "bend", "curve", "change", "direction", "right", "to", "down" ], "content": "\n" }, { "name": "arrow-crossover-down", "keywords": [ "cross", "move", "over", "arrow", "arrows", "down" ], "content": "\n" }, { "name": "arrow-crossover-left", "keywords": [ "cross", "move", "over", "arrow", "arrows", "left" ], "content": "\n" }, { "name": "arrow-crossover-right", "keywords": [ "cross", "move", "over", "arrow", "arrows", "ight" ], "content": "\n" }, { "name": "arrow-crossover-up", "keywords": [ "cross", "move", "over", "arrow", "arrows", "right" ], "content": "\n" }, { "name": "arrow-cursor-1", "keywords": [ "mouse", "select", "cursor" ], "content": "\n" }, { "name": "arrow-cursor-2", "keywords": [ "mouse", "select", "cursor" ], "content": "\n" }, { "name": "arrow-curvy-up-down-1", "keywords": [ "both", "direction", "arrow", "curvy", "diagram", "zigzag", "vertical" ], "content": "\n" }, { "name": "arrow-curvy-up-down-2", "keywords": [], "content": "\n" }, { "name": "arrow-down-2", "keywords": [ "down", "move", "arrow", "arrows" ], "content": "\n" }, { "name": "arrow-down-dashed-square", "keywords": [ "arrow", "keyboard", "button", "down", "square", "dashes" ], "content": "\n" }, { "name": "arrow-expand", "keywords": [ "expand", "small", "bigger", "retract", "smaller", "big" ], "content": "\n" }, { "name": "arrow-infinite-loop", "keywords": [ "arrow", "diagram", "loop", "infinity", "repeat" ], "content": "\n" }, { "name": "arrow-move", "keywords": [ "move", "button", "arrows", "direction" ], "content": "\n" }, { "name": "arrow-reload-horizontal-1", "keywords": [ "arrows", "load", "arrow", "sync", "square", "loading", "reload", "synchronize" ], "content": "\n" }, { "name": "arrow-reload-horizontal-2", "keywords": [], "content": "\n" }, { "name": "arrow-reload-vertical-1", "keywords": [ "arrows", "load", "arrow", "sync", "square", "loading", "reload", "synchronize" ], "content": "\n" }, { "name": "arrow-reload-vertical-2", "keywords": [], "content": "\n" }, { "name": "arrow-roadmap", "keywords": [], "content": "\n" }, { "name": "arrow-round-left", "keywords": [ "diagram", "round", "arrow", "left" ], "content": "\n" }, { "name": "arrow-round-right", "keywords": [ "diagram", "round", "arrow", "right" ], "content": "\n" }, { "name": "arrow-shrink", "keywords": [ "expand", "retract", "shrink", "bigger", "big", "small", "smaller" ], "content": "\n" }, { "name": "arrow-shrink-diagonal-1", "keywords": [ "expand", "retract", "shrink", "bigger", "big", "small", "smaller" ], "content": "\n" }, { "name": "arrow-shrink-diagonal-2", "keywords": [ "expand", "retract", "shrink", "bigger", "big", "small", "smaller" ], "content": "\n" }, { "name": "arrow-transfer-diagonal-1", "keywords": [ "arrows", "arrow", "server", "data", "diagonal", "internet", "transfer", "network" ], "content": "\n" }, { "name": "arrow-transfer-diagonal-2", "keywords": [ "arrows", "arrow", "server", "data", "diagonal", "internet", "transfer", "network" ], "content": "\n" }, { "name": "arrow-transfer-diagonal-3", "keywords": [ "arrows", "arrow", "server", "data", "diagonal", "internet", "transfer", "network" ], "content": "\n" }, { "name": "arrow-up-1", "keywords": [ "arrow", "up", "keyboard" ], "content": "\n" }, { "name": "arrow-up-dashed-square", "keywords": [ "arrow", "keyboard", "button", "up", "square", "dashes" ], "content": "\n" }, { "name": "ascending-number-order", "keywords": [], "content": "\n" }, { "name": "attribution", "keywords": [], "content": "\n" }, { "name": "blank-calendar", "keywords": [ "blank", "calendar", "date", "day", "month", "empty" ], "content": "\n" }, { "name": "blank-notepad", "keywords": [ "content", "notes", "book", "notepad", "notebook" ], "content": "\n" }, { "name": "block-bell-notification", "keywords": [ "notification", "alarm", "alert", "bell", "block" ], "content": "\n" }, { "name": "bomb", "keywords": [ "delete", "bomb", "remove" ], "content": "\n" }, { "name": "bookmark", "keywords": [ "bookmarks", "tags", "favorite" ], "content": "\n" }, { "name": "braces-circle", "keywords": [ "interface", "math", "braces", "sign", "mathematics" ], "content": "\n" }, { "name": "brightness-1", "keywords": [ "bright", "adjust", "brightness", "adjustment", "sun", "raise", "controls" ], "content": "\n" }, { "name": "brightness-2", "keywords": [ "bright", "adjust", "brightness", "adjustment", "sun", "raise", "controls", "half" ], "content": "\n" }, { "name": "brightness-3", "keywords": [ "bright", "adjust", "brightness", "adjustment", "sun", "raise", "controls", "dot", "small" ], "content": "\n" }, { "name": "broken-link-2", "keywords": [ "break", "broken", "hyperlink", "link", "remove", "unlink", "chain" ], "content": "\n" }, { "name": "bullet-list", "keywords": [ "points", "bullet", "unordered", "list", "lists", "bullets" ], "content": "\n" }, { "name": "calendar-add", "keywords": [ "add", "calendar", "date", "day", "month" ], "content": "\n" }, { "name": "calendar-edit", "keywords": [ "calendar", "date", "day", "compose", "edit", "note" ], "content": "\n" }, { "name": "calendar-jump-to-date", "keywords": [], "content": "\n" }, { "name": "calendar-star", "keywords": [ "calendar", "date", "day", "favorite", "like", "month", "star" ], "content": "\n" }, { "name": "celsius", "keywords": [ "degrees", "temperature", "centigrade", "celsius", "degree", "weather" ], "content": "\n" }, { "name": "check", "keywords": [ "check", "form", "validation", "checkmark", "success", "add", "addition", "tick" ], "content": "\n" }, { "name": "check-square", "keywords": [ "check", "form", "validation", "checkmark", "success", "add", "addition", "box", "square", "tick" ], "content": "\n" }, { "name": "circle", "keywords": [ "geometric", "circle", "round", "design", "shape", "shapes", "shape" ], "content": "\n" }, { "name": "circle-clock", "keywords": [ "clock", "loading", "measure", "time", "circle" ], "content": "\n" }, { "name": "clipboard-add", "keywords": [ "edit", "task", "edition", "add", "clipboard", "form" ], "content": "\n" }, { "name": "clipboard-check", "keywords": [ "checkmark", "edit", "task", "edition", "checklist", "check", "success", "clipboard", "form" ], "content": "\n" }, { "name": "clipboard-remove", "keywords": [ "edit", "task", "edition", "remove", "delete", "clipboard", "form" ], "content": "\n" }, { "name": "cloud", "keywords": [ "cloud", "meteorology", "cloudy", "overcast", "cover", "weather" ], "content": "\n" }, { "name": "cog", "keywords": [ "work", "loading", "cog", "gear", "settings", "machine" ], "content": "\n" }, { "name": "color-palette", "keywords": [ "color", "palette", "company", "office", "supplies", "work" ], "content": "\n" }, { "name": "color-picker", "keywords": [ "color", "colors", "design", "dropper", "eye", "eyedrop", "eyedropper", "painting", "picker" ], "content": "\n" }, { "name": "color-swatches", "keywords": [ "color", "colors", "design", "painting", "palette", "sample", "swatch" ], "content": "\n" }, { "name": "cone-shape", "keywords": [], "content": "\n" }, { "name": "convert-PDF-2", "keywords": [ "essential", "files", "folder", "convert", "to", "PDF" ], "content": "\n" }, { "name": "copy-paste", "keywords": [ "clipboard", "copy", "cut", "paste" ], "content": "\n" }, { "name": "creative-commons", "keywords": [], "content": "\n" }, { "name": "crop-selection", "keywords": [ "artboard", "crop", "design", "image", "picture" ], "content": "\n" }, { "name": "crown", "keywords": [ "reward", "social", "rating", "media", "queen", "vip", "king", "crown" ], "content": "\n" }, { "name": "customer-support-1", "keywords": [ "customer", "headset", "help", "microphone", "phone", "support" ], "content": "\n" }, { "name": "cut", "keywords": [ "coupon", "cut", "discount", "price", "prices", "scissors" ], "content": "\n" }, { "name": "dark-dislay-mode", "keywords": [], "content": "\n" }, { "name": "dashboard-3", "keywords": [ "app", "application", "dashboard", "home", "layout", "vertical" ], "content": "\n" }, { "name": "dashboard-circle", "keywords": [ "app", "application", "dashboard", "home", "layout", "circle" ], "content": "\n" }, { "name": "delete-1", "keywords": [ "remove", "add", "button", "buttons", "delete", "cross", "x", "mathematics", "multiply", "math" ], "content": "\n" }, { "name": "descending-number-order", "keywords": [], "content": "\n" }, { "name": "disable-bell-notification", "keywords": [ "disable", "silent", "notification", "off", "silence", "alarm", "bell", "alert" ], "content": "\n" }, { "name": "disable-heart", "keywords": [], "content": "\n" }, { "name": "division-circle", "keywords": [ "interface", "math", "divided", "by", "sign", "mathematics" ], "content": "\n" }, { "name": "download-box-1", "keywords": [ "arrow", "box", "down", "download", "internet", "network", "server", "upload" ], "content": "\n" }, { "name": "download-circle", "keywords": [ "arrow", "circle", "down", "download", "internet", "network", "server", "upload" ], "content": "\n" }, { "name": "download-computer", "keywords": [ "action", "actions", "computer", "desktop", "device", "display", "download", "monitor", "screen" ], "content": "\n" }, { "name": "download-file", "keywords": [], "content": "\n" }, { "name": "empty-clipboard", "keywords": [ "work", "plain", "clipboard", "task", "list", "company", "office" ], "content": "\n" }, { "name": "equal-sign", "keywords": [ "interface", "math", "equal", "sign", "mathematics" ], "content": "\n" }, { "name": "expand", "keywords": [ "big", "bigger", "design", "expand", "larger", "resize", "size", "square" ], "content": "\n" }, { "name": "expand-horizontal-1", "keywords": [ "expand", "resize", "bigger", "horizontal", "smaller", "size", "arrow", "arrows", "big" ], "content": "\n" }, { "name": "expand-window-2", "keywords": [ "expand", "small", "bigger", "retract", "smaller", "big" ], "content": "\n" }, { "name": "face-scan-1", "keywords": [ "identification", "angle", "secure", "human", "id", "person", "face", "security", "brackets" ], "content": "\n" }, { "name": "factorial", "keywords": [ "interface", "math", "number", "factorial", "sign", "mathematics" ], "content": "\n" }, { "name": "fahrenheit", "keywords": [ "degrees", "temperature", "fahrenheit", "degree", "weather" ], "content": "\n" }, { "name": "fastforward-clock", "keywords": [ "time", "clock", "reset", "stopwatch", "circle", "measure", "loading" ], "content": "\n" }, { "name": "file-add-alternate", "keywords": [ "file", "common", "add" ], "content": "\n" }, { "name": "file-delete-alternate", "keywords": [ "file", "common", "delete", "cross" ], "content": "\n" }, { "name": "file-remove-alternate", "keywords": [ "file", "common", "remove", "minus", "subtract" ], "content": "\n" }, { "name": "filter-2", "keywords": [ "funnel", "filter", "angle", "oil" ], "content": "\n" }, { "name": "fingerprint-1", "keywords": [ "identification", "password", "touch", "id", "secure", "fingerprint", "finger", "security" ], "content": "\n" }, { "name": "fingerprint-2", "keywords": [ "identification", "password", "touch", "id", "secure", "fingerprint", "finger", "security" ], "content": "\n" }, { "name": "fist", "keywords": [], "content": "\n" }, { "name": "fit-to-height-square", "keywords": [], "content": "\n" }, { "name": "flip-vertical-arrow-2", "keywords": [ "arrow", "design", "flip", "reflect", "up", "down" ], "content": "\n" }, { "name": "flip-vertical-circle-1", "keywords": [ "flip", "bottom", "object", "work" ], "content": "\n" }, { "name": "flip-vertical-square-2", "keywords": [ "design", "up", "flip", "reflect", "vertical" ], "content": "\n" }, { "name": "folder-add", "keywords": [ "add", "folder", "plus" ], "content": "\n" }, { "name": "folder-check", "keywords": [ "remove", "check", "folder" ], "content": "\n" }, { "name": "folder-delete", "keywords": [ "remove", "minus", "folder", "subtract", "delete" ], "content": "\n" }, { "name": "front-camera", "keywords": [], "content": "\n" }, { "name": "gif-format", "keywords": [], "content": "\n" }, { "name": "give-gift", "keywords": [ "reward", "social", "rating", "media", "queen", "vip", "gift" ], "content": "\n" }, { "name": "glasses", "keywords": [ "vision", "sunglasses", "protection", "spectacles", "correction", "sun", "eye", "glasses" ], "content": "\n" }, { "name": "half-star-1", "keywords": [ "reward", "rating", "rate", "social", "star", "media", "favorite", "like", "stars", "half" ], "content": "\n" }, { "name": "hand-cursor", "keywords": [ "hand", "select", "cursor", "finger" ], "content": "\n" }, { "name": "hand-grab", "keywords": [ "hand", "select", "cursor", "finger", "grab" ], "content": "\n" }, { "name": "heading-1-paragraph-styles-heading", "keywords": [], "content": "\n" }, { "name": "heading-2-paragraph-styles-heading", "keywords": [], "content": "\n" }, { "name": "heading-3-paragraph-styles-heading", "keywords": [], "content": "\n" }, { "name": "heart", "keywords": [ "reward", "social", "rating", "media", "heart", "it", "like", "favorite", "love" ], "content": "\n" }, { "name": "help-chat-2", "keywords": [ "bubble", "help", "mark", "message", "query", "question", "speech", "circle" ], "content": "\n" }, { "name": "help-question-1", "keywords": [ "circle", "faq", "frame", "help", "info", "mark", "more", "query", "question" ], "content": "\n" }, { "name": "hierarchy-10", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n" }, { "name": "hierarchy-13", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n" }, { "name": "hierarchy-14", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n" }, { "name": "hierarchy-2", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n" }, { "name": "hierarchy-4", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n" }, { "name": "hierarchy-7", "keywords": [ "node", "organization", "links", "structure", "link", "nodes", "network", "hierarchy" ], "content": "\n" }, { "name": "home-3", "keywords": [ "home", "house", "roof", "shelter" ], "content": "\n" }, { "name": "home-4", "keywords": [ "home", "house", "roof", "shelter" ], "content": "\n" }, { "name": "horizontal-menu-circle", "keywords": [ "navigation", "dots", "three", "circle", "button", "horizontal", "menu" ], "content": "\n" }, { "name": "humidity-none", "keywords": [ "humidity", "drop", "weather" ], "content": "\n" }, { "name": "image-blur", "keywords": [], "content": "\n" }, { "name": "image-saturation", "keywords": [], "content": "\n" }, { "name": "information-circle", "keywords": [ "information", "frame", "info", "more", "help", "point", "circle" ], "content": "\n" }, { "name": "input-box", "keywords": [ "cursor", "text", "formatting", "type", "format" ], "content": "\n" }, { "name": "insert-side", "keywords": [ "points", "bullet", "align", "paragraph", "formatting", "bullets", "text" ], "content": "\n" }, { "name": "insert-top-left", "keywords": [ "alignment", "wrap", "formatting", "paragraph", "image", "left", "text" ], "content": "\n" }, { "name": "insert-top-right", "keywords": [ "paragraph", "image", "text", "alignment", "wrap", "right", "formatting" ], "content": "\n" }, { "name": "invisible-1", "keywords": [ "disable", "eye", "eyeball", "hide", "off", "view" ], "content": "\n" }, { "name": "invisible-2", "keywords": [], "content": "\n" }, { "name": "jump-object", "keywords": [], "content": "\n" }, { "name": "key", "keywords": [ "entry", "key", "lock", "login", "pass", "unlock", "access" ], "content": "\n" }, { "name": "keyhole-lock-circle", "keywords": [ "circle", "frame", "key", "keyhole", "lock", "locked", "secure", "security" ], "content": "\n" }, { "name": "lasso-tool", "keywords": [], "content": "\n" }, { "name": "layers-1", "keywords": [ "design", "layer", "layers", "pile", "stack", "align" ], "content": "\n" }, { "name": "layers-2", "keywords": [ "design", "layer", "layers", "pile", "stack", "align" ], "content": "\n" }, { "name": "layout-window-1", "keywords": [ "column", "layout", "layouts", "left", "sidebar" ], "content": "\n" }, { "name": "layout-window-11", "keywords": [], "content": "\n" }, { "name": "layout-window-2", "keywords": [ "column", "header", "layout", "layouts", "masthead", "sidebar" ], "content": "\n" }, { "name": "layout-window-8", "keywords": [ "grid", "header", "layout", "layouts", "masthead" ], "content": "\n" }, { "name": "lightbulb", "keywords": [ "lighting", "light", "incandescent", "bulb", "lights" ], "content": "\n" }, { "name": "like-1", "keywords": [ "reward", "social", "up", "rating", "media", "like", "thumb", "hand" ], "content": "\n" }, { "name": "link-chain", "keywords": [ "create", "hyperlink", "link", "make", "unlink", "connection", "chain" ], "content": "\n" }, { "name": "live-video", "keywords": [], "content": "\n" }, { "name": "lock-rotation", "keywords": [], "content": "\n" }, { "name": "login-1", "keywords": [ "arrow", "enter", "frame", "left", "login", "point", "rectangle" ], "content": "\n" }, { "name": "logout-1", "keywords": [ "arrow", "exit", "frame", "leave", "logout", "rectangle", "right" ], "content": "\n" }, { "name": "loop-1", "keywords": [ "multimedia", "multi", "button", "repeat", "media", "loop", "infinity", "controls" ], "content": "\n" }, { "name": "magic-wand-2", "keywords": [ "design", "magic", "star", "supplies", "tool", "wand" ], "content": "\n" }, { "name": "magnifying-glass", "keywords": [ "glass", "search", "magnifying" ], "content": "\n" }, { "name": "magnifying-glass-circle", "keywords": [ "circle", "glass", "search", "magnifying" ], "content": "\n" }, { "name": "manual-book", "keywords": [], "content": "\n" }, { "name": "megaphone-2", "keywords": [ "bullhorn", "loud", "megaphone", "share", "speaker", "transmit" ], "content": "\n" }, { "name": "minimize-window-2", "keywords": [ "expand", "retract", "shrink", "bigger", "big", "small", "smaller" ], "content": "\n" }, { "name": "moon-cloud", "keywords": [ "cloud", "meteorology", "cloudy", "partly", "sunny", "weather" ], "content": "\n" }, { "name": "move-left", "keywords": [ "move", "left", "arrows" ], "content": "\n" }, { "name": "move-right", "keywords": [ "move", "right", "arrows" ], "content": "\n" }, { "name": "multiple-file-2", "keywords": [ "double", "common", "file" ], "content": "\n" }, { "name": "music-folder-song", "keywords": [], "content": "\n" }, { "name": "new-file", "keywords": [ "empty", "common", "file", "content" ], "content": "\n" }, { "name": "new-folder", "keywords": [ "empty", "folder" ], "content": "\n" }, { "name": "new-sticky-note", "keywords": [ "empty", "common", "file" ], "content": "\n" }, { "name": "not-equal-sign", "keywords": [ "interface", "math", "not", "equal", "sign", "mathematics" ], "content": "\n" }, { "name": "ok-hand", "keywords": [], "content": "\n" }, { "name": "one-finger-drag-horizontal", "keywords": [], "content": "\n" }, { "name": "one-finger-drag-vertical", "keywords": [], "content": "\n" }, { "name": "one-finger-hold", "keywords": [], "content": "\n" }, { "name": "one-finger-tap", "keywords": [], "content": "\n" }, { "name": "open-book", "keywords": [ "content", "books", "book", "open" ], "content": "\n" }, { "name": "open-umbrella", "keywords": [ "storm", "rain", "umbrella", "open", "weather" ], "content": "\n" }, { "name": "padlock-square-1", "keywords": [ "combination", "combo", "lock", "locked", "padlock", "secure", "security", "shield", "keyhole" ], "content": "\n" }, { "name": "page-setting", "keywords": [ "page", "setting", "square", "triangle", "circle", "line", "combination", "variation" ], "content": "\n" }, { "name": "paint-bucket", "keywords": [ "bucket", "color", "colors", "design", "paint", "painting" ], "content": "\n" }, { "name": "paint-palette", "keywords": [ "color", "colors", "design", "paint", "painting", "palette" ], "content": "\n" }, { "name": "paintbrush-1", "keywords": [ "brush", "color", "colors", "design", "paint", "painting" ], "content": "\n" }, { "name": "paintbrush-2", "keywords": [ "brush", "color", "colors", "design", "paint", "painting" ], "content": "\n" }, { "name": "paperclip-1", "keywords": [ "attachment", "link", "paperclip", "unlink" ], "content": "\n" }, { "name": "paragraph", "keywords": [ "alignment", "paragraph", "formatting", "text" ], "content": "\n" }, { "name": "pathfinder-divide", "keywords": [], "content": "\n" }, { "name": "pathfinder-exclude", "keywords": [], "content": "\n" }, { "name": "pathfinder-intersect", "keywords": [], "content": "\n" }, { "name": "pathfinder-merge", "keywords": [], "content": "\n" }, { "name": "pathfinder-minus-front-1", "keywords": [], "content": "\n" }, { "name": "pathfinder-trim", "keywords": [], "content": "\n" }, { "name": "pathfinder-union", "keywords": [], "content": "\n" }, { "name": "peace-hand", "keywords": [], "content": "\n" }, { "name": "pen-3", "keywords": [ "content", "creation", "edit", "pen", "pens", "write" ], "content": "\n" }, { "name": "pen-draw", "keywords": [], "content": "\n" }, { "name": "pen-tool", "keywords": [], "content": "\n" }, { "name": "pencil", "keywords": [ "change", "edit", "modify", "pencil", "write", "writing" ], "content": "\n" }, { "name": "pentagon", "keywords": [ "pentagon", "design", "geometric", "shape", "shapes", "shape" ], "content": "\n" }, { "name": "pi-symbol-circle", "keywords": [ "interface", "math", "pi", "sign", "mathematics", "22", "7" ], "content": "\n" }, { "name": "pictures-folder-memories", "keywords": [], "content": "\n" }, { "name": "podium", "keywords": [ "work", "desk", "notes", "company", "presentation", "office", "podium", "microphone" ], "content": "\n" }, { "name": "polygon", "keywords": [ "polygon", "octangle", "design", "geometric", "shape", "shapes", "shape" ], "content": "\n" }, { "name": "praying-hand", "keywords": [], "content": "\n" }, { "name": "projector-board", "keywords": [ "projector", "screen", "work", "meeting", "presentation" ], "content": "\n" }, { "name": "pyramid-shape", "keywords": [], "content": "\n" }, { "name": "quotation-2", "keywords": [ "quote", "quotation", "format", "formatting", "open", "close", "marks", "text" ], "content": "\n" }, { "name": "radioactive-2", "keywords": [ "warning", "radioactive", "radiation", "emergency", "danger", "safety" ], "content": "\n" }, { "name": "rain-cloud", "keywords": [ "cloud", "rain", "rainy", "meteorology", "precipitation", "weather" ], "content": "\n" }, { "name": "recycle-bin-2", "keywords": [ "remove", "delete", "empty", "bin", "trash", "garbage" ], "content": "\n" }, { "name": "ringing-bell-notification", "keywords": [ "notification", "vibrate", "ring", "sound", "alarm", "alert", "bell", "noise" ], "content": "\n" }, { "name": "rock-and-roll-hand", "keywords": [], "content": "\n" }, { "name": "rotate-angle-45", "keywords": [ "rotate", "angle", "company", "office", "supplies", "work" ], "content": "\n" }, { "name": "round-cap", "keywords": [], "content": "\n" }, { "name": "satellite-dish", "keywords": [ "broadcast", "satellite", "share", "transmit", "satellite" ], "content": "\n" }, { "name": "scanner", "keywords": [], "content": "\n" }, { "name": "search-visual", "keywords": [], "content": "\n" }, { "name": "select-circle-area-1", "keywords": [ "select", "area", "object", "work" ], "content": "\n" }, { "name": "share-link", "keywords": [ "share", "transmit" ], "content": "\n" }, { "name": "shield-1", "keywords": [ "shield", "protection", "security", "defend", "crime", "war", "cover" ], "content": "\n" }, { "name": "shield-2", "keywords": [ "shield", "protection", "security", "defend", "crime", "war", "cover" ], "content": "\n" }, { "name": "shield-check", "keywords": [ "shield", "protection", "security", "defend", "crime", "war", "cover", "check" ], "content": "\n" }, { "name": "shield-cross", "keywords": [ "shield", "secure", "security", "cross", "add", "plus" ], "content": "\n" }, { "name": "shrink-horizontal-1", "keywords": [ "resize", "shrink", "bigger", "horizontal", "smaller", "size", "arrow", "arrows", "big" ], "content": "\n" }, { "name": "shuffle", "keywords": [ "multimedia", "shuffle", "multi", "button", "controls", "media" ], "content": "\n" }, { "name": "sigma", "keywords": [ "formula", "text", "format", "sigma", "formatting", "sum" ], "content": "\n" }, { "name": "skull-1", "keywords": [ "crash", "death", "delete", "die", "error", "garbage", "remove", "skull", "trash" ], "content": "\n" }, { "name": "sleep", "keywords": [], "content": "\n" }, { "name": "snow-flake", "keywords": [ "winter", "freeze", "snow", "freezing", "ice", "cold", "weather", "snowflake" ], "content": "\n" }, { "name": "sort-descending", "keywords": [], "content": "\n" }, { "name": "spiral-shape", "keywords": [], "content": "\n" }, { "name": "split-vertical", "keywords": [], "content": "\n" }, { "name": "spray-paint", "keywords": [ "can", "color", "colors", "design", "paint", "painting", "spray" ], "content": "\n" }, { "name": "square-brackets-circle", "keywords": [ "interface", "math", "brackets", "sign", "mathematics" ], "content": "\n" }, { "name": "square-cap", "keywords": [], "content": "\n" }, { "name": "square-clock", "keywords": [ "clock", "loading", "frame", "measure", "time", "circle" ], "content": "\n" }, { "name": "square-root-x-circle", "keywords": [ "interface", "math", "square", "root", "sign", "mathematics" ], "content": "\n" }, { "name": "star-1", "keywords": [ "reward", "rating", "rate", "social", "star", "media", "favorite", "like", "stars" ], "content": "\n" }, { "name": "star-2", "keywords": [ "reward", "rating", "rate", "social", "star", "media", "favorite", "like", "stars", "spark" ], "content": "\n" }, { "name": "star-badge", "keywords": [ "ribbon", "reward", "like", "social", "rating", "media" ], "content": "\n" }, { "name": "straight-cap", "keywords": [], "content": "\n" }, { "name": "subtract-1", "keywords": [ "button", "delete", "buttons", "subtract", "horizontal", "remove", "line", "add", "mathematics", "math", "minus" ], "content": "\n" }, { "name": "subtract-circle", "keywords": [ "delete", "add", "circle", "subtract", "button", "buttons", "remove", "mathematics", "math", "minus" ], "content": "\n" }, { "name": "subtract-square", "keywords": [ "subtract", "buttons", "remove", "add", "button", "square", "delete", "mathematics", "math", "minus" ], "content": "\n" }, { "name": "sun-cloud", "keywords": [ "cloud", "meteorology", "cloudy", "partly", "sunny", "weather" ], "content": "\n" }, { "name": "synchronize-disable", "keywords": [ "arrows", "loading", "load", "sync", "synchronize", "arrow", "reload" ], "content": "\n" }, { "name": "synchronize-warning", "keywords": [ "arrow", "fail", "notification", "sync", "warning", "failure", "synchronize", "error" ], "content": "\n" }, { "name": "table-lamp-1", "keywords": [ "lighting", "light", "incandescent", "bulb", "lights", "table", "lamp" ], "content": "\n" }, { "name": "tag", "keywords": [ "tags", "bookmark", "favorite" ], "content": "\n" }, { "name": "text-flow-rows", "keywords": [], "content": "\n" }, { "name": "text-square", "keywords": [ "text", "options", "formatting", "format", "square", "color", "border", "fill" ], "content": "\n" }, { "name": "text-style", "keywords": [ "text", "style", "formatting", "format" ], "content": "\n" }, { "name": "thermometer", "keywords": [ "temperature", "thermometer", "weather", "level", "meter", "mercury", "measure" ], "content": "\n" }, { "name": "trending-content", "keywords": [ "lit", "flame", "torch", "trending" ], "content": "\n" }, { "name": "trophy", "keywords": [ "reward", "rating", "trophy", "social", "award", "media" ], "content": "\n" }, { "name": "two-finger-drag-hotizontal", "keywords": [], "content": "\n" }, { "name": "two-finger-tap", "keywords": [], "content": "\n" }, { "name": "underline-text-1", "keywords": [ "text", "underline", "formatting", "format" ], "content": "\n" }, { "name": "upload-box-1", "keywords": [ "arrow", "box", "download", "internet", "network", "server", "up", "upload" ], "content": "\n" }, { "name": "upload-circle", "keywords": [ "arrow", "circle", "download", "internet", "network", "server", "up", "upload" ], "content": "\n" }, { "name": "upload-computer", "keywords": [ "action", "actions", "computer", "desktop", "device", "display", "monitor", "screen", "upload" ], "content": "\n" }, { "name": "upload-file", "keywords": [], "content": "\n" }, { "name": "user-add-plus", "keywords": [ "actions", "add", "close", "geometric", "human", "person", "plus", "single", "up", "user" ], "content": "\n" }, { "name": "user-check-validate", "keywords": [ "actions", "close", "checkmark", "check", "geometric", "human", "person", "single", "success", "up", "user" ], "content": "\n" }, { "name": "user-circle-single", "keywords": [ "circle", "geometric", "human", "person", "single", "user" ], "content": "\n" }, { "name": "user-identifier-card", "keywords": [], "content": "\n" }, { "name": "user-multiple-circle", "keywords": [ "close", "geometric", "human", "multiple", "person", "up", "user", "circle" ], "content": "\n" }, { "name": "user-multiple-group", "keywords": [ "close", "geometric", "human", "multiple", "person", "up", "user" ], "content": "\n" }, { "name": "user-profile-focus", "keywords": [ "close", "geometric", "human", "person", "profile", "focus", "user" ], "content": "\n" }, { "name": "user-protection-2", "keywords": [ "shield", "secure", "security", "profile", "person" ], "content": "\n" }, { "name": "user-remove-subtract", "keywords": [ "actions", "remove", "close", "geometric", "human", "person", "minus", "single", "up", "user" ], "content": "\n" }, { "name": "user-single-neutral-male", "keywords": [ "close", "geometric", "human", "person", "single", "up", "user", "male" ], "content": "\n" }, { "name": "user-sync-online-in-person", "keywords": [], "content": "\n" }, { "name": "vertical-slider-square", "keywords": [ "adjustment", "adjust", "controls", "fader", "vertical", "settings", "slider", "square" ], "content": "\n" }, { "name": "video-swap-camera", "keywords": [], "content": "\n" }, { "name": "visible", "keywords": [ "eye", "eyeball", "open", "view" ], "content": "\n" }, { "name": "voice-scan-2", "keywords": [ "identification", "secure", "id", "soundwave", "sound", "voice", "brackets", "security" ], "content": "\n" }, { "name": "waning-cresent-moon", "keywords": [ "night", "new", "moon", "crescent", "weather", "time", "waning" ], "content": "\n" }, { "name": "warning-octagon", "keywords": [ "frame", "alert", "warning", "octagon", "exclamation", "caution" ], "content": "\n" }, { "name": "warning-triangle", "keywords": [ "frame", "alert", "warning", "triangle", "exclamation", "caution" ], "content": "\n" } ], "mail": [ { "name": "chat-bubble-oval", "keywords": [ "messages", "message", "bubble", "chat", "oval" ], "content": "\n" }, { "name": "chat-bubble-oval-notification", "keywords": [ "messages", "message", "bubble", "chat", "oval", "notify", "ping" ], "content": "\n" }, { "name": "chat-bubble-oval-smiley-1", "keywords": [ "messages", "message", "bubble", "chat", "oval", "smiley", "smile" ], "content": "\n" }, { "name": "chat-bubble-oval-smiley-2", "keywords": [ "messages", "message", "bubble", "chat", "oval", "smiley", "smile" ], "content": "\n" }, { "name": "chat-bubble-square-block", "keywords": [ "messages", "message", "bubble", "chat", "square", "block" ], "content": "\n" }, { "name": "chat-bubble-square-question", "keywords": [ "bubble", "square", "messages", "notification", "chat", "message", "question", "help" ], "content": "\n" }, { "name": "chat-bubble-square-warning", "keywords": [ "bubble", "square", "messages", "notification", "chat", "message", "warning", "alert" ], "content": "\n" }, { "name": "chat-bubble-square-write", "keywords": [ "messages", "message", "bubble", "chat", "square", "write", "review", "pen", "pencil", "compose" ], "content": "\n" }, { "name": "chat-bubble-text-square", "keywords": [ "messages", "message", "bubble", "text", "square", "chat" ], "content": "\n" }, { "name": "chat-bubble-typing-oval", "keywords": [ "messages", "message", "bubble", "typing", "chat" ], "content": "\n" }, { "name": "chat-two-bubbles-oval", "keywords": [ "messages", "message", "bubble", "chat", "oval", "conversation" ], "content": "\n" }, { "name": "discussion-converstion-reply", "keywords": [], "content": "\n" }, { "name": "happy-face", "keywords": [ "smiley", "chat", "message", "smile", "emoji", "face", "satisfied" ], "content": "\n" }, { "name": "inbox-block", "keywords": [ "mail", "envelope", "email", "message", "block", "spam", "remove" ], "content": "\n" }, { "name": "inbox-favorite", "keywords": [ "mail", "envelope", "email", "message", "star", "favorite", "important", "bookmark" ], "content": "\n" }, { "name": "inbox-favorite-heart", "keywords": [ "mail", "envelope", "email", "message", "heart", "favorite", "like", "love", "important", "bookmark" ], "content": "\n" }, { "name": "inbox-lock", "keywords": [ "mail", "envelope", "email", "message", "secure", "password", "lock", "encryption" ], "content": "\n" }, { "name": "inbox-tray-1", "keywords": [ "mail", "email", "outbox", "drawer", "empty", "open", "inbox", "arrow", "down" ], "content": "\n" }, { "name": "inbox-tray-2", "keywords": [ "mail", "email", "outbox", "drawer", "empty", "open", "inbox", "arrow", "up" ], "content": "\n" }, { "name": "mail-incoming", "keywords": [ "inbox", "envelope", "email", "message", "down", "arrow", "inbox" ], "content": "\n" }, { "name": "mail-search", "keywords": [ "inbox", "envelope", "email", "message", "search" ], "content": "\n" }, { "name": "mail-send-email-message", "keywords": [ "send", "email", "paper", "airplane", "deliver" ], "content": "\n" }, { "name": "mail-send-envelope", "keywords": [ "envelope", "email", "message", "unopened", "sealed", "close" ], "content": "\n" }, { "name": "mail-send-reply-all", "keywords": [ "email", "message", "reply", "all", "actions", "action", "arrow" ], "content": "\n" }, { "name": "sad-face", "keywords": [ "smiley", "chat", "message", "emoji", "sad", "face", "unsatisfied" ], "content": "\n" }, { "name": "send-email", "keywords": [ "mail", "send", "email", "paper", "airplane" ], "content": "\n" }, { "name": "sign-at", "keywords": [ "mail", "email", "at", "sign", "read", "address" ], "content": "\n" }, { "name": "sign-hashtag", "keywords": [ "mail", "sharp", "sign", "hashtag", "tag" ], "content": "\n" }, { "name": "smiley-angry", "keywords": [], "content": "\n" }, { "name": "smiley-cool", "keywords": [], "content": "\n" }, { "name": "smiley-crying-1", "keywords": [], "content": "\n" }, { "name": "smiley-cute", "keywords": [], "content": "\n" }, { "name": "smiley-drool", "keywords": [], "content": "\n" }, { "name": "smiley-emoji-kiss-nervous", "keywords": [], "content": "\n" }, { "name": "smiley-emoji-terrified", "keywords": [], "content": "\n" }, { "name": "smiley-grumpy", "keywords": [], "content": "\n" }, { "name": "smiley-happy", "keywords": [], "content": "\n" }, { "name": "smiley-in-love", "keywords": [], "content": "\n" }, { "name": "smiley-kiss", "keywords": [], "content": "\n" }, { "name": "smiley-laughing-3", "keywords": [], "content": "\n" } ], "map_travel": [ { "name": "airplane", "keywords": [ "travel", "plane", "adventure", "airplane", "transportation" ], "content": "\n" }, { "name": "airport-plane-transit", "keywords": [], "content": "\n" }, { "name": "airport-plane", "keywords": [], "content": "\n" }, { "name": "airport-security", "keywords": [], "content": "\n" }, { "name": "anchor", "keywords": [ "anchor", "marina", "harbor", "port", "travel", "places" ], "content": "\n" }, { "name": "baggage", "keywords": [ "check", "baggage", "travel", "adventure", "luggage", "bag", "checked", "airport" ], "content": "\n" }, { "name": "beach", "keywords": [ "island", "waves", "outdoor", "recreation", "tree", "beach", "palm", "wave", "water", "travel", "places" ], "content": "\n" }, { "name": "bicycle-bike", "keywords": [], "content": "\n" }, { "name": "braille-blind", "keywords": [ "disability", "braille", "blind" ], "content": "\n" }, { "name": "bus", "keywords": [ "transportation", "travel", "bus", "transit", "transport", "motorcoach", "public" ], "content": "\n" }, { "name": "camping-tent", "keywords": [ "outdoor", "recreation", "camping", "tent", "teepee", "tipi", "travel", "places" ], "content": "\n" }, { "name": "cane", "keywords": [ "disability", "cane" ], "content": "\n" }, { "name": "capitol", "keywords": [ "capitol", "travel", "places" ], "content": "\n" }, { "name": "car-battery-charging", "keywords": [], "content": "\n" }, { "name": "car-taxi-1", "keywords": [ "transportation", "travel", "taxi", "transport", "cab", "car" ], "content": "\n" }, { "name": "city-hall", "keywords": [ "city", "hall", "travel", "places" ], "content": "\n" }, { "name": "compass-navigator", "keywords": [], "content": "\n" }, { "name": "crutch", "keywords": [ "disability", "crutch" ], "content": "\n" }, { "name": "dangerous-zone-sign", "keywords": [], "content": "\n" }, { "name": "earth-1", "keywords": [ "planet", "earth", "globe", "world" ], "content": "\n" }, { "name": "earth-airplane", "keywords": [ "travel", "plane", "trip", "airplane", "international", "adventure", "globe", "world", "airport" ], "content": "\n" }, { "name": "emergency-exit", "keywords": [], "content": "\n" }, { "name": "fire-alarm-2", "keywords": [], "content": "\n" }, { "name": "fire-extinguisher-sign", "keywords": [], "content": "\n" }, { "name": "gas-station-fuel-petroleum", "keywords": [], "content": "\n" }, { "name": "hearing-deaf-1", "keywords": [ "disability", "hearing", "deaf" ], "content": "\n" }, { "name": "hearing-deaf-2", "keywords": [ "disability", "hearing", "deaf" ], "content": "\n" }, { "name": "high-speed-train-front", "keywords": [], "content": "\n" }, { "name": "hot-spring", "keywords": [ "relax", "location", "outdoor", "recreation", "spa", "travel", "places" ], "content": "\n" }, { "name": "hotel-air-conditioner", "keywords": [ "heating", "ac", "air", "hvac", "cool", "cooling", "cold", "hot", "conditioning", "hotel" ], "content": "\n" }, { "name": "hotel-bed-2", "keywords": [ "bed", "double", "bedroom", "bedrooms", "queen", "king", "full", "hotel", "hotel" ], "content": "\n" }, { "name": "hotel-laundry", "keywords": [ "laundry", "machine", "hotel" ], "content": "\n" }, { "name": "hotel-one-star", "keywords": [ "one", "star", "reviews", "review", "rating", "hotel", "star" ], "content": "\n" }, { "name": "hotel-shower-head", "keywords": [ "bathe", "bath", "bathroom", "shower", "water", "head", "hotel" ], "content": "\n" }, { "name": "hotel-two-star", "keywords": [ "two", "stars", "reviews", "review", "rating", "hotel", "star" ], "content": "\n" }, { "name": "information-desk-customer", "keywords": [], "content": "\n" }, { "name": "information-desk", "keywords": [], "content": "\n" }, { "name": "iron", "keywords": [ "laundry", "iron", "heat", "hotel" ], "content": "\n" }, { "name": "ladder", "keywords": [ "business", "product", "metaphor", "ladder" ], "content": "\n" }, { "name": "lift", "keywords": [ "arrow", "up", "human", "down", "person", "user", "lift", "elevator" ], "content": "\n" }, { "name": "lift-disability", "keywords": [ "arrow", "up", "human", "down", "person", "user", "lift", "elevator", "disability", "wheelchair", "accessible" ], "content": "\n" }, { "name": "location-compass-1", "keywords": [ "arrow", "compass", "location", "gps", "map", "maps", "point" ], "content": "\n" }, { "name": "location-pin-3", "keywords": [ "navigation", "map", "maps", "pin", "gps", "location" ], "content": "\n" }, { "name": "location-pin-disabled", "keywords": [ "navigation", "map", "maps", "pin", "gps", "location", "disabled", "off" ], "content": "\n" }, { "name": "location-target-1", "keywords": [ "navigation", "location", "map", "services", "maps", "gps", "target" ], "content": "\n" }, { "name": "lost-and-found", "keywords": [], "content": "\n" }, { "name": "man-symbol", "keywords": [ "geometric", "gender", "boy", "person", "male", "human", "user" ], "content": "\n" }, { "name": "map-fold", "keywords": [ "navigation", "map", "maps", "gps", "travel", "fold" ], "content": "\n" }, { "name": "navigation-arrow-off", "keywords": [ "compass", "arrow", "map", "bearing", "navigation", "maps", "heading", "gps", "off", "disable" ], "content": "\n" }, { "name": "navigation-arrow-on", "keywords": [ "compass", "arrow", "map", "bearing", "navigation", "maps", "heading", "gps", "off", "disable" ], "content": "\n" }, { "name": "parking-sign", "keywords": [ "discount", "coupon", "parking", "price", "prices", "hotel" ], "content": "\n" }, { "name": "parliament", "keywords": [ "travel", "places", "parliament" ], "content": "\n" }, { "name": "passport", "keywords": [ "travel", "book", "id", "adventure", "visa", "airport" ], "content": "\n" }, { "name": "pet-paw", "keywords": [ "paw", "foot", "animals", "pets", "footprint", "track", "hotel" ], "content": "\n" }, { "name": "pets-allowed", "keywords": [ "travel", "wayfinder", "pets", "allowed" ], "content": "\n" }, { "name": "pool-ladder", "keywords": [ "pool", "stairs", "swim", "swimming", "water", "ladder", "hotel" ], "content": "\n" }, { "name": "rock-slide", "keywords": [ "hill", "cliff", "sign", "danger", "stone" ], "content": "\n" }, { "name": "sail-ship", "keywords": [ "travel", "boat", "transportation", "transport", "ocean", "ship", "sea", "water" ], "content": "\n" }, { "name": "school-bus-side", "keywords": [], "content": "\n" }, { "name": "smoke-detector", "keywords": [ "smoke", "alert", "fire", "signal" ], "content": "\n" }, { "name": "smoking-area", "keywords": [], "content": "\n" }, { "name": "snorkle", "keywords": [ "diving", "scuba", "outdoor", "recreation", "ocean", "mask", "water", "sea", "snorkle", "travel", "places" ], "content": "\n" }, { "name": "steering-wheel", "keywords": [], "content": "\n" }, { "name": "street-road", "keywords": [], "content": "\n" }, { "name": "street-sign", "keywords": [ "crossroad", "street", "sign", "metaphor", "directions", "travel", "places" ], "content": "\n" }, { "name": "take-off", "keywords": [ "travel", "plane", "adventure", "airplane", "take", "off", "airport" ], "content": "\n" }, { "name": "toilet-man", "keywords": [ "travel", "wayfinder", "toilet", "man" ], "content": "\n" }, { "name": "toilet-sign-man-woman-2", "keywords": [ "toilet", "sign", "restroom", "bathroom", "user", "human", "person" ], "content": "\n" }, { "name": "toilet-women", "keywords": [ "travel", "wayfinder", "toilet", "women" ], "content": "\n" }, { "name": "traffic-cone", "keywords": [ "street", "sign", "traffic", "cone", "road" ], "content": "\n" }, { "name": "triangle-flag", "keywords": [ "navigation", "map", "maps", "flag", "gps", "location", "destination", "goal" ], "content": "\n" }, { "name": "wheelchair-1", "keywords": [ "person", "access", "wheelchair", "accomodation", "human", "disability", "disabled", "user" ], "content": "\n" }, { "name": "woman-symbol", "keywords": [ "geometric", "gender", "female", "person", "human", "user" ], "content": "\n" } ], "money_shopping": [ { "name": "annoncement-megaphone", "keywords": [], "content": "\n" }, { "name": "backpack", "keywords": [ "bag", "backpack", "school", "baggage", "cloth", "clothing", "accessories" ], "content": "\n" }, { "name": "bag", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n" }, { "name": "bag-dollar", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n" }, { "name": "bag-pound", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n" }, { "name": "bag-rupee", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n" }, { "name": "bag-suitcase-1", "keywords": [ "product", "business", "briefcase" ], "content": "\n" }, { "name": "bag-suitcase-2", "keywords": [ "product", "business", "briefcase" ], "content": "\n" }, { "name": "bag-yen", "keywords": [ "bag", "payment", "cash", "money", "finance" ], "content": "\n" }, { "name": "ball", "keywords": [ "sports", "ball", "sport", "basketball", "shopping", "catergories" ], "content": "\n" }, { "name": "bank", "keywords": [ "institution", "saving", "bank", "payment", "finance" ], "content": "\n" }, { "name": "beanie", "keywords": [ "beanie", "winter", "hat", "warm", "cloth", "clothing", "wearable", "accessories" ], "content": "\n" }, { "name": "bill-1", "keywords": [ "billing", "bills", "payment", "finance", "cash", "currency", "money", "accounting" ], "content": "\n" }, { "name": "bill-2", "keywords": [ "currency", "billing", "payment", "finance", "cash", "bill", "money", "accounting" ], "content": "\n" }, { "name": "bill-4", "keywords": [ "accounting", "billing", "payment", "finance", "cash", "currency", "money", "bill", "dollar", "stack" ], "content": "\n" }, { "name": "bill-cashless", "keywords": [ "currency", "billing", "payment", "finance", "no", "cash", "bill", "money", "accounting", "cashless" ], "content": "\n" }, { "name": "binance-circle", "keywords": [ "crypto", "circle", "payment", "blockchain", "finance", "binance", "currency" ], "content": "\n" }, { "name": "bitcoin", "keywords": [ "crypto", "circle", "payment", "blokchain", "finance", "bitcoin", "money", "currency" ], "content": "\n" }, { "name": "bow-tie", "keywords": [ "bow", "tie", "dress", "gentleman", "cloth", "clothing", "accessories" ], "content": "\n" }, { "name": "briefcase-dollar", "keywords": [ "briefcase", "payment", "cash", "money", "finance", "baggage", "bag" ], "content": "\n" }, { "name": "building-2", "keywords": [ "real", "home", "tower", "building", "house", "estate" ], "content": "\n" }, { "name": "business-card", "keywords": [ "name", "card", "business", "information", "money", "payment" ], "content": "\n" }, { "name": "business-handshake", "keywords": [ "deal", "contract", "business", "money", "payment", "agreement" ], "content": "\n" }, { "name": "business-idea-money", "keywords": [], "content": "\n" }, { "name": "business-profession-home-office", "keywords": [ "workspace", "home", "office", "work", "business", "remote", "working" ], "content": "\n" }, { "name": "business-progress-bar-2", "keywords": [ "business", "production", "arrow", "workflow", "money", "flag", "timeline" ], "content": "\n" }, { "name": "business-user-curriculum", "keywords": [], "content": "\n" }, { "name": "calculator-1", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "calculate", "math" ], "content": "\n" }, { "name": "calculator-2", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "calculate", "math", "sign" ], "content": "\n" }, { "name": "cane", "keywords": [ "walking", "stick", "cane", "accessories", "gentleman", "accessories" ], "content": "\n" }, { "name": "chair", "keywords": [ "chair", "business", "product", "comfort", "decoration", "sit", "furniture" ], "content": "\n" }, { "name": "closet", "keywords": [ "closet", "dressing", "dresser", "product", "decoration", "cloth", "clothing", "cabinet", "furniture" ], "content": "\n" }, { "name": "coin-share", "keywords": [ "payment", "cash", "money", "finance", "receive", "give", "coin", "hand" ], "content": "\n" }, { "name": "coins-stack", "keywords": [ "accounting", "billing", "payment", "stack", "cash", "coins", "currency", "money", "finance" ], "content": "\n" }, { "name": "credit-card-1", "keywords": [ "credit", "pay", "payment", "debit", "card", "finance", "plastic", "money", "atm" ], "content": "\n" }, { "name": "credit-card-2", "keywords": [ "deposit", "payment", "finance", "atm", "withdraw", "atm" ], "content": "\n" }, { "name": "diamond-2", "keywords": [ "diamond", "money", "payment", "finance", "wealth", "jewelry" ], "content": "\n" }, { "name": "discount-percent-badge", "keywords": [ "shop", "shops", "stores", "discount", "coupon" ], "content": "\n" }, { "name": "discount-percent-circle", "keywords": [ "store", "shop", "shops", "stores", "discount", "coupon" ], "content": "\n" }, { "name": "discount-percent-coupon", "keywords": [ "shop", "shops", "stores", "discount", "coupon", "voucher" ], "content": "\n" }, { "name": "discount-percent-cutout", "keywords": [ "store", "shop", "shops", "stores", "discount", "coupon" ], "content": "\n" }, { "name": "discount-percent-fire", "keywords": [ "shop", "shops", "stores", "discount", "coupon", "hot", "trending" ], "content": "\n" }, { "name": "dollar-coin", "keywords": [ "accounting", "billing", "payment", "cash", "coin", "currency", "money", "finance" ], "content": "\n" }, { "name": "dollar-coin-1", "keywords": [ "accounting", "billing", "payment", "cash", "coin", "currency", "money", "finance" ], "content": "\n" }, { "name": "dressing-table", "keywords": [ "makeup", "dressing", "table", "mirror", "cabinet", "product", "decoration", "furniture" ], "content": "\n" }, { "name": "ethereum", "keywords": [ "crypto", "circle", "payment", "blokchain", "finance", "ethereum", "eth", "currency" ], "content": "\n" }, { "name": "ethereum-circle", "keywords": [ "crypto", "circle", "payment", "blockchain", "finance", "eth", "currency" ], "content": "\n" }, { "name": "euro", "keywords": [ "exchange", "payment", "euro", "forex", "finance", "foreign", "currency" ], "content": "\n" }, { "name": "gift", "keywords": [ "reward", "box", "social", "present", "gift", "media", "rating", "bow" ], "content": "\n" }, { "name": "gift-2", "keywords": [ "reward", "box", "social", "present", "gift", "media", "rating", "bow" ], "content": "\n" }, { "name": "gold", "keywords": [ "gold", "money", "payment", "bars", "finance", "wealth", "bullion", "jewelry" ], "content": "\n" }, { "name": "graph", "keywords": [ "analytics", "business", "product", "graph", "data", "chart", "analysis" ], "content": "\n" }, { "name": "graph-arrow-decrease", "keywords": [ "down", "stats", "graph", "descend", "right", "arrow" ], "content": "\n" }, { "name": "graph-arrow-increase", "keywords": [ "ascend", "growth", "up", "arrow", "stats", "graph", "right", "grow" ], "content": "\n" }, { "name": "graph-bar-decrease", "keywords": [ "arrow", "product", "performance", "down", "decrease", "graph", "business", "chart" ], "content": "\n" }, { "name": "graph-bar-increase", "keywords": [ "up", "product", "performance", "increase", "arrow", "graph", "business", "chart" ], "content": "\n" }, { "name": "graph-dot", "keywords": [ "product", "data", "bars", "analysis", "analytics", "graph", "business", "chart", "dot" ], "content": "\n" }, { "name": "investment-selection", "keywords": [], "content": "\n" }, { "name": "justice-hammer", "keywords": [ "hammer", "work", "mallet", "office", "company", "gavel", "justice", "judge", "arbitration", "court" ], "content": "\n" }, { "name": "justice-scale-1", "keywords": [ "office", "work", "scale", "justice", "company", "arbitration", "balance", "court" ], "content": "\n" }, { "name": "justice-scale-2", "keywords": [ "office", "work", "scale", "justice", "unequal", "company", "arbitration", "unbalance", "court" ], "content": "\n" }, { "name": "lipstick", "keywords": [ "fashion", "beauty", "lip", "lipstick", "makeup", "shopping" ], "content": "\n" }, { "name": "make-up-brush", "keywords": [ "fashion", "beauty", "make", "up", "brush" ], "content": "\n" }, { "name": "moustache", "keywords": [ "fashion", "beauty", "moustache", "grooming" ], "content": "\n" }, { "name": "mouth-lip", "keywords": [ "fashion", "beauty", "mouth", "lip" ], "content": "\n" }, { "name": "necklace", "keywords": [ "diamond", "money", "payment", "finance", "wealth", "accessory", "necklace", "jewelry" ], "content": "\n" }, { "name": "necktie", "keywords": [ "necktie", "businessman", "business", "cloth", "clothing", "gentleman", "accessories" ], "content": "\n" }, { "name": "payment-10", "keywords": [ "deposit", "payment", "finance", "atm", "transfer", "dollar" ], "content": "\n" }, { "name": "payment-cash-out-3", "keywords": [], "content": "\n" }, { "name": "pie-chart", "keywords": [ "product", "data", "analysis", "analytics", "pie", "business", "chart" ], "content": "\n" }, { "name": "piggy-bank", "keywords": [ "institution", "saving", "bank", "payment", "finance" ], "content": "\n" }, { "name": "polka-dot-circle", "keywords": [ "crypto", "circle", "payment", "blockchain", "finance", "polka", "dot", "currency" ], "content": "\n" }, { "name": "production-belt", "keywords": [ "production", "produce", "box", "belt", "factory", "product", "package", "business" ], "content": "\n" }, { "name": "qr-code", "keywords": [ "codes", "tags", "code", "qr" ], "content": "\n" }, { "name": "receipt", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "bill", "receipt" ], "content": "\n" }, { "name": "receipt-add", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "bill", "receipt", "add", "plus", "new" ], "content": "\n" }, { "name": "receipt-check", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "bill", "receipt", "check", "confirm" ], "content": "\n" }, { "name": "receipt-subtract", "keywords": [ "shop", "shopping", "pay", "payment", "store", "cash", "bill", "receipt", "subtract", "minus", "remove" ], "content": "\n" }, { "name": "safe-vault", "keywords": [ "saving", "combo", "payment", "safe", "combination", "finance" ], "content": "\n" }, { "name": "scanner-3", "keywords": [ "payment", "electronic", "cash", "dollar", "codes", "tags", "upc", "barcode", "qr" ], "content": "\n" }, { "name": "scanner-bar-code", "keywords": [ "codes", "tags", "upc", "barcode" ], "content": "\n" }, { "name": "shelf", "keywords": [ "shelf", "drawer", "cabinet", "prodcut", "decoration", "furniture" ], "content": "\n" }, { "name": "shopping-bag-hand-bag-2", "keywords": [ "shopping", "bag", "purse", "goods", "item", "products" ], "content": "\n" }, { "name": "shopping-basket-1", "keywords": [ "shopping", "basket" ], "content": "\n" }, { "name": "shopping-basket-2", "keywords": [ "shopping", "basket" ], "content": "\n" }, { "name": "shopping-cart-1", "keywords": [ "shopping", "cart", "checkout" ], "content": "\n" }, { "name": "shopping-cart-2", "keywords": [ "shopping", "cart", "checkout" ], "content": "\n" }, { "name": "shopping-cart-3", "keywords": [ "shopping", "cart", "checkout" ], "content": "\n" }, { "name": "shopping-cart-add", "keywords": [ "shopping", "cart", "checkout", "add", "plus", "new" ], "content": "\n" }, { "name": "shopping-cart-check", "keywords": [ "shopping", "cart", "checkout", "check", "confirm" ], "content": "\n" }, { "name": "shopping-cart-subtract", "keywords": [ "shopping", "cart", "checkout", "subtract", "minus", "remove" ], "content": "\n" }, { "name": "signage-3", "keywords": [ "street", "sandwich", "shops", "shop", "stores", "board", "sign", "store" ], "content": "\n" }, { "name": "signage-4", "keywords": [ "street", "billboard", "shops", "shop", "stores", "board", "sign", "ads", "banner" ], "content": "\n" }, { "name": "startup", "keywords": [ "shop", "rocket", "launch", "startup" ], "content": "\n" }, { "name": "stock", "keywords": [ "price", "stock", "wallstreet", "dollar", "money", "currency", "fluctuate", "candlestick", "business" ], "content": "\n" }, { "name": "store-1", "keywords": [ "store", "shop", "shops", "stores" ], "content": "\n" }, { "name": "store-2", "keywords": [ "store", "shop", "shops", "stores" ], "content": "\n" }, { "name": "store-computer", "keywords": [ "store", "shop", "shops", "stores", "online", "computer", "website", "desktop", "app" ], "content": "\n" }, { "name": "subscription-cashflow", "keywords": [], "content": "\n" }, { "name": "tag", "keywords": [ "codes", "tags", "tag", "product", "label" ], "content": "\n" }, { "name": "tall-hat", "keywords": [ "tall", "hat", "cloth", "clothing", "wearable", "magician", "gentleman", "accessories" ], "content": "\n" }, { "name": "target", "keywords": [ "shop", "bullseye", "arrow", "target" ], "content": "\n" }, { "name": "target-3", "keywords": [ "shop", "bullseye", "shooting", "target" ], "content": "\n" }, { "name": "wallet", "keywords": [ "money", "payment", "finance", "wallet" ], "content": "\n" }, { "name": "wallet-purse", "keywords": [ "money", "payment", "finance", "wallet", "purse" ], "content": "\n" }, { "name": "xrp-circle", "keywords": [ "crypto", "circle", "payment", "blockchain", "finance", "xrp", "currency" ], "content": "\n" }, { "name": "yuan", "keywords": [ "exchange", "payment", "forex", "finance", "yuan", "foreign", "currency" ], "content": "\n" }, { "name": "yuan-circle", "keywords": [ "exchange", "payment", "forex", "finance", "yuan", "foreign", "currency" ], "content": "\n" } ], "nature_ecology": [ { "name": "affordable-and-clean-energy", "keywords": [], "content": "\n" }, { "name": "alien", "keywords": [ "science", "extraterristerial", "life", "form", "space", "universe", "head", "astronomy" ], "content": "\n" }, { "name": "bone", "keywords": [ "nature", "pet", "dog", "bone", "food", "snack" ], "content": "\n" }, { "name": "cat-1", "keywords": [ "nature", "head", "cat", "pet", "animals", "felyne" ], "content": "\n" }, { "name": "circle-flask", "keywords": [ "science", "experiment", "lab", "flask", "chemistry", "solution" ], "content": "\n" }, { "name": "clean-water-and-sanitation", "keywords": [], "content": "\n" }, { "name": "comet", "keywords": [ "nature", "meteor", "fall", "space", "object", "danger" ], "content": "\n" }, { "name": "decent-work-and-economic-growth", "keywords": [], "content": "\n" }, { "name": "dna", "keywords": [ "science", "biology", "experiment", "lab", "science" ], "content": "\n" }, { "name": "erlenmeyer-flask", "keywords": [ "science", "experiment", "lab", "flask", "chemistry", "solution" ], "content": "\n" }, { "name": "flower", "keywords": [ "nature", "plant", "tree", "flower", "petals", "bloom" ], "content": "\n" }, { "name": "galaxy-1", "keywords": [ "science", "space", "universe", "astronomy" ], "content": "\n" }, { "name": "galaxy-2", "keywords": [ "science", "space", "universe", "astronomy" ], "content": "\n" }, { "name": "gender-equality", "keywords": [], "content": "\n" }, { "name": "good-health-and-well-being", "keywords": [], "content": "\n" }, { "name": "industry-innovation-and-infrastructure", "keywords": [], "content": "\n" }, { "name": "leaf", "keywords": [ "nature", "environment", "leaf", "ecology", "plant", "plants", "eco" ], "content": "\n" }, { "name": "log", "keywords": [ "nature", "tree", "plant", "circle", "round", "log" ], "content": "\n" }, { "name": "no-poverty", "keywords": [], "content": "\n" }, { "name": "octopus", "keywords": [ "nature", "sealife", "animals" ], "content": "\n" }, { "name": "planet", "keywords": [ "science", "solar", "system", "ring", "planet", "saturn", "space", "astronomy", "astronomy" ], "content": "\n" }, { "name": "potted-flower-tulip", "keywords": [ "nature", "flower", "plant", "tree", "pot" ], "content": "\n" }, { "name": "quality-education", "keywords": [], "content": "\n" }, { "name": "rainbow", "keywords": [ "nature", "arch", "rain", "colorful", "rainbow", "curve", "half", "circle" ], "content": "\n" }, { "name": "recycle-1", "keywords": [ "nature", "sign", "environment", "protect", "save", "arrows" ], "content": "\n" }, { "name": "reduced-inequalities", "keywords": [], "content": "\n" }, { "name": "rose", "keywords": [ "nature", "flower", "rose", "plant", "tree" ], "content": "\n" }, { "name": "shell", "keywords": [ "nature", "sealife", "animals" ], "content": "\n" }, { "name": "shovel-rake", "keywords": [ "nature", "crops", "plants" ], "content": "\n" }, { "name": "sprout", "keywords": [], "content": "\n" }, { "name": "telescope", "keywords": [ "science", "experiment", "star", "gazing", "sky", "night", "space", "universe", "astronomy", "astronomy" ], "content": "\n" }, { "name": "test-tube", "keywords": [ "science", "experiment", "lab", "chemistry", "test", "tube", "solution" ], "content": "\n" }, { "name": "tidal-wave", "keywords": [ "nature", "ocean", "wave" ], "content": "\n" }, { "name": "tree-2", "keywords": [ "nature", "tree", "plant", "circle", "round", "park" ], "content": "\n" }, { "name": "tree-3", "keywords": [ "nature", "tree", "plant", "cloud", "shape", "park" ], "content": "\n" }, { "name": "volcano", "keywords": [ "nature", "eruption", "erupt", "mountain", "volcano", "lava", "magma", "explosion" ], "content": "\n" }, { "name": "windmill", "keywords": [], "content": "\n" }, { "name": "zero-hunger", "keywords": [], "content": "\n" } ], "phone": [ { "name": "airplane-disabled", "keywords": [ "server", "plane", "airplane", "disabled", "off", "wireless", "mode", "internet", "network" ], "content": "\n" }, { "name": "airplane-enabled", "keywords": [ "server", "plane", "airplane", "enabled", "on", "wireless", "mode", "internet", "network" ], "content": "\n" }, { "name": "back-camera-1", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "camera", "lenses" ], "content": "\n" }, { "name": "call-hang-up", "keywords": [ "phone", "telephone", "mobile", "device", "smartphone", "call", "hang", "up" ], "content": "\n" }, { "name": "cellular-network-4g", "keywords": [], "content": "\n" }, { "name": "cellular-network-5g", "keywords": [], "content": "\n" }, { "name": "cellular-network-lte", "keywords": [], "content": "\n" }, { "name": "contact-phonebook-2", "keywords": [], "content": "\n" }, { "name": "hang-up-1", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "telephone", "hang", "up" ], "content": "\n" }, { "name": "hang-up-2", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "telephone", "hang", "up" ], "content": "\n" }, { "name": "incoming-call", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "ringing", "incoming", "call" ], "content": "\n" }, { "name": "missed-call", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "missed", "call" ], "content": "\n" }, { "name": "notification-alarm-2", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "alert", "bell", "alarm" ], "content": "\n" }, { "name": "notification-application-1", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "alert", "box" ], "content": "\n" }, { "name": "notification-application-2", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "alert", "box" ], "content": "\n" }, { "name": "notification-message-alert", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "alert", "message", "text" ], "content": "\n" }, { "name": "outgoing-call", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "ringing", "outgoing", "call" ], "content": "\n" }, { "name": "phone", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone" ], "content": "\n" }, { "name": "phone-mobile-phone", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone" ], "content": "\n" }, { "name": "phone-qr", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "qr", "code", "scan" ], "content": "\n" }, { "name": "phone-ringing-1", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "ringing", "incoming", "call" ], "content": "\n" }, { "name": "phone-ringing-2", "keywords": [ "android", "phone", "mobile", "device", "smartphone", "iphone", "ringing" ], "content": "\n" }, { "name": "signal-full", "keywords": [ "phone", "mobile", "device", "signal", "wireless", "smartphone", "iphone", "bar", "bars", "full", "android" ], "content": "\n" }, { "name": "signal-low", "keywords": [ "phone", "mobile", "device", "signal", "wireless", "smartphone", "iphone", "bar", "low", "bars", "android" ], "content": "\n" }, { "name": "signal-medium", "keywords": [ "smartphone", "phone", "mobile", "device", "iphone", "signal", "medium", "wireless", "bar", "bars", "android" ], "content": "\n" }, { "name": "signal-none", "keywords": [ "phone", "mobile", "device", "signal", "wireless", "smartphone", "iphone", "bar", "bars", "no", "zero", "android" ], "content": "\n" } ], "programing": [ { "name": "application-add", "keywords": [ "application", "new", "add", "square" ], "content": "\n" }, { "name": "bracket", "keywords": [ "code", "angle", "programming", "file", "bracket" ], "content": "\n" }, { "name": "browser-add", "keywords": [ "app", "code", "apps", "add", "window", "plus" ], "content": "\n" }, { "name": "browser-block", "keywords": [ "block", "access", "denied", "window", "browser", "privacy", "remove" ], "content": "\n" }, { "name": "browser-build", "keywords": [ "build", "website", "development", "window", "code", "web", "backend", "browser", "dev" ], "content": "\n" }, { "name": "browser-check", "keywords": [ "checkmark", "pass", "window", "app", "code", "success", "check", "apps" ], "content": "\n" }, { "name": "browser-delete", "keywords": [ "app", "code", "apps", "fail", "delete", "window", "remove", "cross" ], "content": "\n" }, { "name": "browser-hash", "keywords": [ "window", "hash", "code", "internet", "language", "browser", "web", "tag" ], "content": "\n" }, { "name": "browser-lock", "keywords": [ "secure", "password", "window", "browser", "lock", "security", "login", "encryption" ], "content": "\n" }, { "name": "browser-multiple-window", "keywords": [ "app", "code", "apps", "two", "window", "cascade" ], "content": "\n" }, { "name": "browser-remove", "keywords": [ "app", "code", "apps", "subtract", "window", "minus" ], "content": "\n" }, { "name": "browser-website-1", "keywords": [ "app", "code", "apps", "window", "website", "web" ], "content": "\n" }, { "name": "bug", "keywords": [ "code", "bug", "security", "programming", "secure", "computer" ], "content": "\n" }, { "name": "bug-antivirus-debugging", "keywords": [ "code", "bug", "security", "programming", "secure", "computer", "antivirus", "block", "protection", "malware", "debugging" ], "content": "\n" }, { "name": "bug-antivirus-shield", "keywords": [ "code", "bug", "security", "programming", "secure", "computer", "antivirus", "shield", "protection", "malware" ], "content": "\n" }, { "name": "bug-virus-browser", "keywords": [ "bug", "browser", "file", "virus", "threat", "danger", "internet" ], "content": "\n" }, { "name": "bug-virus-document", "keywords": [ "bug", "document", "file", "virus", "threat", "danger" ], "content": "\n" }, { "name": "bug-virus-folder", "keywords": [ "bug", "document", "folder", "virus", "threat", "danger" ], "content": "\n" }, { "name": "cloud-add", "keywords": [ "cloud", "network", "internet", "add", "server", "plus" ], "content": "\n" }, { "name": "cloud-block", "keywords": [ "cloud", "network", "internet", "block", "server", "deny" ], "content": "\n" }, { "name": "cloud-check", "keywords": [ "cloud", "network", "internet", "check", "server", "approve" ], "content": "\n" }, { "name": "cloud-data-transfer", "keywords": [ "cloud", "data", "transfer", "internet", "server", "network" ], "content": "\n" }, { "name": "cloud-refresh", "keywords": [ "cloud", "network", "internet", "server", "refresh" ], "content": "\n" }, { "name": "cloud-share", "keywords": [ "cloud", "network", "internet", "server", "share" ], "content": "\n" }, { "name": "cloud-warning", "keywords": [ "cloud", "network", "internet", "server", "warning", "alert" ], "content": "\n" }, { "name": "cloud-wifi", "keywords": [ "cloud", "wifi", "internet", "server", "network" ], "content": "\n" }, { "name": "code-analysis", "keywords": [], "content": "\n" }, { "name": "code-monitor-1", "keywords": [ "code", "tags", "angle", "bracket", "monitor" ], "content": "\n" }, { "name": "code-monitor-2", "keywords": [ "code", "tags", "angle", "image", "ui", "ux", "design" ], "content": "\n" }, { "name": "css-three", "keywords": [ "language", "three", "code", "programming", "html", "css" ], "content": "\n" }, { "name": "curly-brackets", "keywords": [], "content": "\n" }, { "name": "file-code-1", "keywords": [ "code", "files", "angle", "programming", "file", "bracket" ], "content": "\n" }, { "name": "incognito-mode", "keywords": [ "internet", "safe", "mode", "browser" ], "content": "\n" }, { "name": "insert-cloud-video", "keywords": [], "content": "\n" }, { "name": "markdown-circle-programming", "keywords": [], "content": "\n" }, { "name": "markdown-document-programming", "keywords": [], "content": "\n" }, { "name": "module-puzzle-1", "keywords": [ "code", "puzzle", "module", "programming", "plugin", "piece" ], "content": "\n" }, { "name": "module-puzzle-3", "keywords": [ "code", "puzzle", "module", "programming", "plugin", "piece" ], "content": "\n" }, { "name": "module-three", "keywords": [ "code", "three", "module", "programming", "plugin" ], "content": "\n" }, { "name": "rss-square", "keywords": [ "wireless", "rss", "feed", "square", "transmit", "broadcast" ], "content": "\n" } ], "shipping": [ { "name": "box-sign", "keywords": [ "box", "package", "label", "delivery", "shipment", "shipping", "this", "way", "up", "arrow", "sign", "sticker" ], "content": "\n" }, { "name": "container", "keywords": [ "box", "package", "label", "delivery", "shipment", "shipping", "container" ], "content": "\n" }, { "name": "fragile", "keywords": [ "fragile", "shipping", "glass", "delivery", "wine", "crack", "shipment", "sign", "sticker" ], "content": "\n" }, { "name": "parachute-drop", "keywords": [ "package", "box", "fulfillment", "cart", "warehouse", "shipping", "delivery", "drop", "parachute" ], "content": "\n" }, { "name": "shipment-add", "keywords": [ "shipping", "parcel", "shipment", "add" ], "content": "\n" }, { "name": "shipment-check", "keywords": [ "shipping", "parcel", "shipment", "check", "approved" ], "content": "\n" }, { "name": "shipment-download", "keywords": [ "shipping", "parcel", "shipment", "download" ], "content": "\n" }, { "name": "shipment-remove", "keywords": [ "shipping", "parcel", "shipment", "remove", "subtract" ], "content": "\n" }, { "name": "shipment-upload", "keywords": [ "shipping", "parcel", "shipment", "upload" ], "content": "\n" }, { "name": "shipping-box-1", "keywords": [ "box", "package", "label", "delivery", "shipment", "shipping" ], "content": "\n" }, { "name": "shipping-truck", "keywords": [ "truck", "shipping", "delivery", "transfer" ], "content": "\n" }, { "name": "transfer-motorcycle", "keywords": [ "motorcycle", "shipping", "delivery", "courier", "transfer" ], "content": "\n" }, { "name": "transfer-van", "keywords": [ "van", "shipping", "delivery", "transfer" ], "content": "\n" }, { "name": "warehouse-1", "keywords": [ "delivery", "warehouse", "shipping", "fulfillment" ], "content": "\n" } ], "work_education": [ { "name": "book-reading", "keywords": [ "book", "reading", "learning" ], "content": "\n" }, { "name": "class-lesson", "keywords": [ "class", "lesson", "education", "teacher" ], "content": "\n" }, { "name": "collaborations-idea", "keywords": [ "collaborations", "idea", "work" ], "content": "\n" }, { "name": "definition-search-book", "keywords": [], "content": "\n" }, { "name": "dictionary-language-book", "keywords": [], "content": "\n" }, { "name": "global-learning", "keywords": [ "global", "learning", "education" ], "content": "\n" }, { "name": "graduation-cap", "keywords": [ "graduation", "cap", "education" ], "content": "\n" }, { "name": "group-meeting-call", "keywords": [ "group", "meeting", "call", "work" ], "content": "\n" }, { "name": "office-building-1", "keywords": [ "office", "building", "work" ], "content": "\n" }, { "name": "office-worker", "keywords": [ "office", "worker", "human", "resources" ], "content": "\n" }, { "name": "search-dollar", "keywords": [ "search", "pay", "product", "currency", "query", "magnifying", "cash", "business", "money", "glass" ], "content": "\n" }, { "name": "strategy-tasks", "keywords": [ "strategy", "tasks", "work" ], "content": "\n" }, { "name": "task-list", "keywords": [ "task", "list", "work" ], "content": "\n" }, { "name": "workspace-desk", "keywords": [ "workspace", "desk", "work" ], "content": "\n" } ] }
\ No newline at end of file
diff --git a/frontend/appflowy_flutter/build.yaml b/frontend/appflowy_flutter/build.yaml
new file mode 100644
index 0000000000000..e69de29bb2d1d
diff --git a/frontend/appflowy_flutter/dart_dependency_validator.yaml b/frontend/appflowy_flutter/dart_dependency_validator.yaml
new file mode 100644
index 0000000000000..cb1df68bb60c2
--- /dev/null
+++ b/frontend/appflowy_flutter/dart_dependency_validator.yaml
@@ -0,0 +1,12 @@
+# dart_dependency_validator.yaml
+
+allow_pins: true
+
+include:
+ - "lib/**"
+
+exclude:
+ - "packages/**"
+
+ignore:
+ - analyzer
diff --git a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart
index 0c8b96fa20d92..00a02788dae3e 100644
--- a/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart
+++ b/frontend/appflowy_flutter/integration_test/cloud/anon_user_continue_test.dart
@@ -3,8 +3,6 @@
import 'dart:io';
import 'dart:ui';
-import 'package:flutter/material.dart';
-
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -12,6 +10,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
+import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/shared/settings_body.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
@@ -19,6 +18,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
+import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:intl/intl.dart';
@@ -42,7 +42,7 @@ void main() {
await tester.tapAnonymousSignInButton();
await tester.expectToSeeHomePageWithGetStartedPage();
- // reanme the name of the anon user
+ // rename the name of the anon user
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
await tester.pumpAndSettle();
@@ -51,12 +51,12 @@ void main() {
// Scroll to sign-in
await tester.scrollUntilVisible(
- find.byType(SignInOutButton),
+ find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
- await tester.tapButton(find.byType(SignInOutButton));
+ await tester.tapButton(find.byType(AccountSignInOutButton));
// sign up with Google
await tester.tapGoogleLoginInButton();
@@ -68,7 +68,7 @@ void main() {
// Scroll to sign-out
await tester.scrollUntilVisible(
- find.byType(SignInOutButton),
+ find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
@@ -85,7 +85,7 @@ void main() {
await tester.openSettings();
await tester.openSettingsPage(SettingsPage.account);
final userNameInput =
- tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
+ tester.widget(find.byType(AccountUserProfile)) as AccountUserProfile;
expect(userNameInput.name, 'Me');
});
});
diff --git a/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart
index 5aa3a02d83fce..dacc900103d87 100644
--- a/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart
+++ b/frontend/appflowy_flutter/integration_test/cloud/appflowy_cloud_auth_test.dart
@@ -6,6 +6,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
+import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:easy_localization/easy_localization.dart';
@@ -41,17 +42,17 @@ void main() {
// Scroll to sign-out
await tester.scrollUntilVisible(
- find.byType(SignInOutButton),
+ find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
- await tester.tapButton(find.byType(SignInOutButton));
+ await tester.tapButton(find.byType(AccountSignInOutButton));
- tester.expectToSeeText(LocaleKeys.button_confirm.tr());
- await tester.tapButtonWithName(LocaleKeys.button_confirm.tr());
+ tester.expectToSeeText(LocaleKeys.button_ok.tr());
+ await tester.tapButtonWithName(LocaleKeys.button_ok.tr());
// Go to the sign in page again
- await tester.pumpAndSettle(const Duration(seconds: 1));
+ await tester.pumpAndSettle(const Duration(seconds: 5));
tester.expectToSeeGoogleLoginButton();
});
@@ -67,11 +68,11 @@ void main() {
// Scroll to sign-in
await tester.scrollUntilVisible(
- find.byType(SignInOutButton),
+ find.byType(AccountSignInOutButton),
100,
scrollable: find.findSettingsScrollable(),
);
- await tester.tapButton(find.byType(SignInOutButton));
+ await tester.tapButton(find.byType(AccountSignInOutButton));
tester.expectToSeeGoogleLoginButton();
});
diff --git a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart
index 15c9c3c3479f6..71cbc1143175f 100644
--- a/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart
+++ b/frontend/appflowy_flutter/integration_test/cloud/supabase_auth_test.dart
@@ -1,93 +1,93 @@
-import 'package:appflowy/env/cloud_env.dart';
-import 'package:appflowy/workspace/application/settings/prelude.dart';
-import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
-import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-
-import '../shared/util.dart';
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
-
- group('supabase auth', () {
- testWidgets('sign in with supabase', (tester) async {
- await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
- await tester.tapGoogleLoginInButton();
- await tester.expectToSeeHomePageWithGetStartedPage();
- });
-
- testWidgets('sign out with supabase', (tester) async {
- await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
- await tester.tapGoogleLoginInButton();
-
- // Open the setting page and sign out
- await tester.openSettings();
- await tester.openSettingsPage(SettingsPage.account);
- await tester.logout();
-
- // Go to the sign in page again
- await tester.pumpAndSettle(const Duration(seconds: 1));
- tester.expectToSeeGoogleLoginButton();
- });
-
- testWidgets('sign in as anonymous', (tester) async {
- await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
- await tester.tapSignInAsGuest();
-
- // should not see the sync setting page when sign in as anonymous
- await tester.openSettings();
- await tester.openSettingsPage(SettingsPage.account);
-
- // Scroll to sign-out
- await tester.scrollUntilVisible(
- find.byType(SignInOutButton),
- 100,
- scrollable: find.findSettingsScrollable(),
- );
- await tester.tapButton(find.byType(SignInOutButton));
-
- tester.expectToSeeGoogleLoginButton();
- });
-
- // testWidgets('enable encryption', (tester) async {
- // await tester.initializeAppFlowy(cloudType: CloudType.supabase);
- // await tester.tapGoogleLoginInButton();
-
- // // Open the setting page and sign out
- // await tester.openSettings();
- // await tester.openSettingsPage(SettingsPage.cloud);
-
- // // the switch should be off by default
- // tester.assertEnableEncryptSwitchValue(false);
- // await tester.toggleEnableEncrypt();
-
- // // the switch should be on after toggling
- // tester.assertEnableEncryptSwitchValue(true);
-
- // // the switch can not be toggled back to off
- // await tester.toggleEnableEncrypt();
- // tester.assertEnableEncryptSwitchValue(true);
- // });
-
- testWidgets('enable sync', (tester) async {
- await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
- await tester.tapGoogleLoginInButton();
-
- // Open the setting page and sign out
- await tester.openSettings();
- await tester.openSettingsPage(SettingsPage.cloud);
-
- // the switch should be on by default
- tester.assertSupabaseEnableSyncSwitchValue(true);
- await tester.toggleEnableSync(SupabaseEnableSync);
-
- // the switch should be off
- tester.assertSupabaseEnableSyncSwitchValue(false);
-
- // the switch should be on after toggling
- await tester.toggleEnableSync(SupabaseEnableSync);
- tester.assertSupabaseEnableSyncSwitchValue(true);
- });
- });
-}
+// import 'package:appflowy/env/cloud_env.dart';
+// import 'package:appflowy/workspace/application/settings/prelude.dart';
+// import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
+// import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
+// import 'package:flutter_test/flutter_test.dart';
+// import 'package:integration_test/integration_test.dart';
+
+// import '../shared/util.dart';
+
+// void main() {
+// IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+// group('supabase auth', () {
+// testWidgets('sign in with supabase', (tester) async {
+// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
+// await tester.tapGoogleLoginInButton();
+// await tester.expectToSeeHomePageWithGetStartedPage();
+// });
+
+// testWidgets('sign out with supabase', (tester) async {
+// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
+// await tester.tapGoogleLoginInButton();
+
+// // Open the setting page and sign out
+// await tester.openSettings();
+// await tester.openSettingsPage(SettingsPage.account);
+// await tester.logout();
+
+// // Go to the sign in page again
+// await tester.pumpAndSettle(const Duration(seconds: 1));
+// tester.expectToSeeGoogleLoginButton();
+// });
+
+// testWidgets('sign in as anonymous', (tester) async {
+// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
+// await tester.tapSignInAsGuest();
+
+// // should not see the sync setting page when sign in as anonymous
+// await tester.openSettings();
+// await tester.openSettingsPage(SettingsPage.account);
+
+// // Scroll to sign-out
+// await tester.scrollUntilVisible(
+// find.byType(SignInOutButton),
+// 100,
+// scrollable: find.findSettingsScrollable(),
+// );
+// await tester.tapButton(find.byType(SignInOutButton));
+
+// tester.expectToSeeGoogleLoginButton();
+// });
+
+// // testWidgets('enable encryption', (tester) async {
+// // await tester.initializeAppFlowy(cloudType: CloudType.supabase);
+// // await tester.tapGoogleLoginInButton();
+
+// // // Open the setting page and sign out
+// // await tester.openSettings();
+// // await tester.openSettingsPage(SettingsPage.cloud);
+
+// // // the switch should be off by default
+// // tester.assertEnableEncryptSwitchValue(false);
+// // await tester.toggleEnableEncrypt();
+
+// // // the switch should be on after toggling
+// // tester.assertEnableEncryptSwitchValue(true);
+
+// // // the switch can not be toggled back to off
+// // await tester.toggleEnableEncrypt();
+// // tester.assertEnableEncryptSwitchValue(true);
+// // });
+
+// testWidgets('enable sync', (tester) async {
+// await tester.initializeAppFlowy(cloudType: AuthenticatorType.supabase);
+// await tester.tapGoogleLoginInButton();
+
+// // Open the setting page and sign out
+// await tester.openSettings();
+// await tester.openSettingsPage(SettingsPage.cloud);
+
+// // the switch should be on by default
+// tester.assertSupabaseEnableSyncSwitchValue(true);
+// await tester.toggleEnableSync(SupabaseEnableSync);
+
+// // the switch should be off
+// tester.assertSupabaseEnableSyncSwitchValue(false);
+
+// // the switch should be on after toggling
+// await tester.toggleEnableSync(SupabaseEnableSync);
+// tester.assertSupabaseEnableSyncSwitchValue(true);
+// });
+// });
+// }
diff --git a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart
index 5791803a0eeac..253d533607404 100644
--- a/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart
+++ b/frontend/appflowy_flutter/integration_test/cloud/user_setting_sync_test.dart
@@ -2,8 +2,6 @@
import 'dart:io';
-import 'package:flutter/material.dart';
-
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
@@ -11,6 +9,7 @@ import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
+import 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/user_avatar.dart';
@@ -18,6 +17,7 @@ import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flowy_infra_ui/style_widget/text_field.dart';
+import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:path/path.dart' as p;
@@ -47,13 +47,12 @@ void main() {
await tester.openSettingsPage(SettingsPage.account);
await tester.enterUserName(name);
- await tester.tapEscButton();
+ await tester.pumpAndSettle(const Duration(seconds: 6));
+ await tester.logout();
- // wait 2 seconds for the sync to finish
await tester.pumpAndSettle(const Duration(seconds: 2));
});
});
-
testWidgets('get user icon and name from server', (tester) async {
await tester.initializeAppFlowy(
cloudType: AuthenticatorType.appflowyCloudSelfHost,
@@ -68,7 +67,7 @@ void main() {
// Verify name
final profileSetting =
- tester.widget(find.byType(UserProfileSetting)) as UserProfileSetting;
+ tester.widget(find.byType(AccountUserProfile)) as AccountUserProfile;
expect(profileSetting.name, name);
});
diff --git a/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart
index 3fe48b5f6f70a..387c35adf2af6 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/board/board_field_test.dart
@@ -22,7 +22,7 @@ void main() {
const fieldName = "test change field";
await tester.createField(
FieldType.RichText,
- fieldName,
+ name: fieldName,
layout: ViewLayoutPB.Board,
);
await tester.tapButton(card1);
diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart
index cb8338fbb6df1..28f50bf817f16 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_cell_test.dart
@@ -1,3 +1,4 @@
+import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flutter_test/flutter_test.dart';
@@ -41,7 +42,7 @@ void main() {
name: 'my grid',
layout: ViewLayoutPB.Grid,
);
- await tester.createField(FieldType.RichText, 'description');
+ await tester.createField(FieldType.RichText, name: 'description');
await tester.editCell(
rowIndex: 0,
@@ -81,7 +82,7 @@ void main() {
const fieldType = FieldType.Number;
// Create a number field
- await tester.createField(fieldType, fieldType.name);
+ await tester.createField(fieldType);
await tester.editCell(
rowIndex: 0,
@@ -157,7 +158,7 @@ void main() {
const fieldType = FieldType.CreatedTime;
// Create a create time field
// The create time field is not editable
- await tester.createField(fieldType, fieldType.name);
+ await tester.createField(fieldType);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
@@ -175,7 +176,7 @@ void main() {
const fieldType = FieldType.LastEditedTime;
// Create a last time field
// The last time field is not editable
- await tester.createField(fieldType, fieldType.name);
+ await tester.createField(fieldType);
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
@@ -191,7 +192,7 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.DateTime;
- await tester.createField(fieldType, fieldType.name);
+ await tester.createField(fieldType);
// Tap the cell to invoke the field editor
await tester.tapCellInGrid(rowIndex: 0, fieldType: fieldType);
@@ -366,7 +367,7 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.MultiSelect;
- await tester.createField(fieldType, fieldType.name);
+ await tester.createField(fieldType, name: fieldType.i18n);
// Tap the cell to invoke the selection option editor
await tester.tapSelectOptionCellInGrid(rowIndex: 0, fieldType: fieldType);
@@ -449,7 +450,7 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
const fieldType = FieldType.Checklist;
- await tester.createField(fieldType, fieldType.name);
+ await tester.createField(fieldType);
// assert that there is no progress bar in the grid
tester.assertChecklistCellInGrid(rowIndex: 0, percent: null);
@@ -461,22 +462,22 @@ void main() {
tester.assertChecklistEditorVisible(visible: true);
// create a new task with enter
- await tester.createNewChecklistTask(name: "task 0", enter: true);
+ await tester.createNewChecklistTask(name: "task 1", enter: true);
// assert that the task is displayed
tester.assertChecklistTaskInEditor(
index: 0,
- name: "task 0",
+ name: "task 1",
isChecked: false,
);
// update the task's name
- await tester.renameChecklistTask(index: 0, name: "task 1");
+ await tester.renameChecklistTask(index: 0, name: "task 11");
// assert that the task's name is updated
tester.assertChecklistTaskInEditor(
index: 0,
- name: "task 1",
+ name: "task 11",
isChecked: false,
);
diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart
index cc1187da21b31..45d05207ff067 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_field_test.dart
@@ -1,12 +1,13 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
import 'package:appflowy/plugins/database/widgets/field/type_option_editor/select/select_option.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
-import 'package:intl/intl.dart';
import '../../shared/database_test_op.dart';
import '../../shared/util.dart';
@@ -56,11 +57,22 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// create a field
- await tester.createField(FieldType.Checklist, 'checklist');
+ await tester.createField(FieldType.Checklist);
+ tester.findFieldWithName(FieldType.Checklist.i18n);
- // check the field is created successfully
- tester.findFieldWithName('checklist');
- await tester.pumpAndSettle();
+ // editing field type during field creation should change title
+ await tester.createField(FieldType.MultiSelect);
+ tester.findFieldWithName(FieldType.MultiSelect.i18n);
+
+ // not if the user changes the title manually though
+ const name = "New field";
+ await tester.createField(FieldType.DateTime);
+ await tester.tapGridFieldWithName(FieldType.DateTime.i18n);
+ await tester.renameField(name);
+ await tester.tapEditFieldButton();
+ await tester.tapSwitchFieldTypeButton();
+ await tester.selectFieldType(FieldType.URL);
+ tester.findFieldWithName(name);
});
testWidgets('delete field', (tester) async {
@@ -70,14 +82,14 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// create a field
- await tester.createField(FieldType.Checkbox, 'New field 1');
+ await tester.createField(FieldType.Checkbox, name: 'New field 1');
// Delete the field
await tester.tapGridFieldWithName('New field 1');
await tester.tapDeletePropertyButton();
// confirm delete
- await tester.tapDialogOkButton();
+ await tester.tapButtonWithName(LocaleKeys.space_delete.tr());
tester.noFieldWithName('New field 1');
await tester.pumpAndSettle();
@@ -90,10 +102,7 @@ void main() {
await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
// create a field
- await tester.scrollToRight(find.byType(GridPage));
- await tester.tapNewPropertyButton();
- await tester.renameField('New field 1');
- await tester.dismissFieldEditor();
+ await tester.createField(FieldType.RichText, name: 'New field 1');
// duplicate the field
await tester.tapGridFieldWithName('New field 1');
@@ -126,26 +135,6 @@ void main() {
await tester.pumpAndSettle();
});
- testWidgets('create checklist field', (tester) async {
- await tester.initializeAppFlowy();
- await tester.tapAnonymousSignInButton();
-
- await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
-
- await tester.scrollToRight(find.byType(GridPage));
- await tester.tapNewPropertyButton();
-
- // Open the type option menu
- await tester.tapSwitchFieldTypeButton();
-
- await tester.selectFieldType(FieldType.Checklist);
-
- // After update the field type, the cells should be updated
- await tester.findCellByFieldType(FieldType.Checklist);
-
- await tester.pumpAndSettle();
- });
-
testWidgets('create list of fields', (tester) async {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
@@ -162,18 +151,10 @@ void main() {
FieldType.CreatedTime,
FieldType.Checkbox,
]) {
- await tester.scrollToRight(find.byType(GridPage));
- await tester.tapNewPropertyButton();
- await tester.renameField(fieldType.name);
-
- // Open the type option menu
- await tester.tapSwitchFieldTypeButton();
-
- await tester.selectFieldType(fieldType);
- await tester.dismissFieldEditor();
+ await tester.createField(fieldType);
// After update the field type, the cells should be updated
- await tester.findCellByFieldType(fieldType);
+ tester.findCellByFieldType(fieldType);
await tester.pumpAndSettle();
}
});
@@ -190,15 +171,7 @@ void main() {
FieldType.Checklist,
FieldType.URL,
]) {
- // create the field
- await tester.scrollToRight(find.byType(GridPage));
- await tester.tapNewPropertyButton();
- await tester.renameField(fieldType.i18n);
-
- // change field type
- await tester.tapSwitchFieldTypeButton();
- await tester.selectFieldType(fieldType);
- await tester.dismissFieldEditor();
+ await tester.createField(fieldType);
// open the field editor
await tester.tapGridFieldWithName(fieldType.i18n);
@@ -218,11 +191,7 @@ void main() {
await tester.scrollToRight(find.byType(GridPage));
// create a number field
- await tester.tapNewPropertyButton();
- await tester.renameField("Number");
- await tester.tapSwitchFieldTypeButton();
- await tester.selectFieldType(FieldType.Number);
- await tester.dismissFieldEditor();
+ await tester.createField(FieldType.Number);
// enter some data into the first number cell
await tester.editCell(
@@ -243,7 +212,7 @@ void main() {
);
// open editor and change number format
- await tester.tapGridFieldWithName('Number');
+ await tester.tapGridFieldWithName(FieldType.Number.i18n);
await tester.tapEditFieldButton();
await tester.changeNumberFieldFormat();
await tester.dismissFieldEditor();
@@ -292,11 +261,7 @@ void main() {
await tester.scrollToRight(find.byType(GridPage));
// create a date field
- await tester.tapNewPropertyButton();
- await tester.renameField(FieldType.DateTime.i18n);
- await tester.tapSwitchFieldTypeButton();
- await tester.selectFieldType(FieldType.DateTime);
- await tester.dismissFieldEditor();
+ await tester.createField(FieldType.DateTime);
// edit the first date cell
await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.DateTime);
diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_media_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_media_test.dart
new file mode 100644
index 0000000000000..ddbffa6ca743d
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_media_test.dart
@@ -0,0 +1,96 @@
+import 'dart:io';
+
+import 'package:flutter/services.dart';
+
+import 'package:appflowy/core/config/kv.dart';
+import 'package:appflowy/core/config/kv_keys.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy_backend/protobuf/flowy-database2/field_entities.pbenum.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:integration_test/integration_test.dart';
+import 'package:path/path.dart' as p;
+import 'package:path_provider/path_provider.dart';
+
+import '../../shared/database_test_op.dart';
+import '../../shared/mock/mock_file_picker.dart';
+import '../../shared/util.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+
+ group('media type option in database', () {
+ testWidgets('add media field and add files', (tester) async {
+ await tester.initializeAppFlowy();
+ await tester.tapAnonymousSignInButton();
+
+ await tester.createNewPageWithNameUnderParent(layout: ViewLayoutPB.Grid);
+
+ // Invoke the field editor
+ await tester.tapGridFieldWithName('Type');
+ await tester.tapEditFieldButton();
+
+ // Change to media type
+ await tester.tapSwitchFieldTypeButton();
+ await tester.selectFieldType(FieldType.Media);
+ await tester.dismissFieldEditor();
+
+ // Open media cell editor
+ await tester.tapCellInGrid(rowIndex: 0, fieldType: FieldType.Media);
+ await tester.findMediaCellEditor(findsOneWidget);
+
+ // Prepare files for upload from local
+ final firstImage =
+ await rootBundle.load('assets/test/images/sample.jpeg');
+ final secondImage =
+ await rootBundle.load('assets/test/images/sample.gif');
+ final tempDirectory = await getTemporaryDirectory();
+
+ final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
+ final firstFile = File(firstImagePath)
+ ..writeAsBytesSync(firstImage.buffer.asUint8List());
+
+ final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
+ final secondFile = File(secondImagePath)
+ ..writeAsBytesSync(secondImage.buffer.asUint8List());
+
+ mockPickFilePaths(paths: [firstImagePath]);
+ await getIt().set(KVKeys.kCloudType, '0');
+
+ // Click on add file button in the Media Cell Editor
+ await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
+ await tester.pumpAndSettle();
+
+ // Tap on the upload interaction
+ await tester.tapButtonWithName(
+ LocaleKeys.document_plugins_file_fileUploadHint.tr(),
+ );
+ await tester.pumpAndSettle();
+
+ // Expect one file
+ expect(find.byType(RenderMedia), findsOneWidget);
+
+ // Mock second file
+ mockPickFilePaths(paths: [secondImagePath]);
+
+ // Click on add file button in the Media Cell Editor
+ await tester.tap(find.text(LocaleKeys.grid_media_addFileOrImage.tr()));
+ await tester.pumpAndSettle();
+
+ // Tap on the upload interaction
+ await tester.tapButtonWithName(
+ LocaleKeys.document_plugins_file_fileUploadHint.tr(),
+ );
+ await tester.pumpAndSettle();
+
+ // Expect two files
+ expect(find.byType(RenderMedia), findsNWidgets(2));
+
+ // Remove the temp files
+ await Future.wait([firstFile.delete(), secondFile.delete()]);
+ });
+ });
+}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart
index 0934e7721be86..095d633b2dbac 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/database/database_row_page_test.dart
@@ -1,3 +1,6 @@
+import 'package:appflowy/plugins/database/grid/presentation/widgets/header/desktop_field_cell.dart';
+import 'package:appflowy/plugins/database/widgets/row/row_detail.dart';
+import 'package:appflowy/util/field_type_extension.dart';
import 'package:flutter/material.dart';
import 'package:appflowy/plugins/database/widgets/row/row_banner.dart';
@@ -121,15 +124,24 @@ void main() {
FieldType.Checkbox,
]) {
await tester.tapRowDetailPageCreatePropertyButton();
- await tester.renameField(fieldType.name);
// Open the type option menu
await tester.tapSwitchFieldTypeButton();
await tester.selectFieldType(fieldType);
+ final field = find.descendant(
+ of: find.byType(RowDetailPage),
+ matching: find.byWidgetPredicate(
+ (widget) =>
+ widget is FieldCellButton &&
+ widget.field.name == fieldType.i18n,
+ ),
+ );
+ expect(field, findsOneWidget);
+
// After update the field type, the cells should be updated
- await tester.findCellByFieldType(fieldType);
+ tester.findCellByFieldType(fieldType);
await tester.scrollRowDetailByOffset(const Offset(0, -50));
}
});
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart
index f06a273fac989..0fb8cc90e84de 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_codeblock_paste_test.dart
@@ -1,10 +1,10 @@
import 'dart:io';
-import 'package:flutter/services.dart';
-
import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -25,6 +25,7 @@ void main() {
const lines = 3;
final text = List.generate(lines, (index) => 'line $index').join('\n');
AppFlowyClipboard.mockSetData(AppFlowyClipboardData(text: text));
+ ClipboardService.mockSetData(ClipboardServiceData(plainText: text));
await insertCodeBlockInDocument(tester);
@@ -51,7 +52,9 @@ Future insertCodeBlockInDocument(WidgetTester tester) async {
// open the actions menu and insert the codeBlock
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
- LocaleKeys.document_selectionMenu_codeBlock.tr(),
+ LocaleKeys.document_slashMenu_name_code.tr(),
+ offset: 150,
);
+ // wait for the codeBlock to be inserted
await tester.pumpAndSettle();
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart
index 730d776460640..457934dff4f5b 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_copy_and_paste_test.dart
@@ -1,11 +1,10 @@
import 'dart:io';
-import 'package:flutter/services.dart';
-
import 'package:appflowy/plugins/document/presentation/editor_plugins/copy_and_paste/clipboard_service.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:appflowy_editor_plugins/appflowy_editor_plugins.dart';
+import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -166,6 +165,44 @@ void main() {
});
});
+ 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();
@@ -246,10 +283,6 @@ void main() {
expect(editorState.document.root.children.length, 2);
final node = editorState.getNodeAtPath([0])!;
expect(node.type, ImageBlockKeys.type);
- expect(
- node.attributes[ImageBlockKeys.url],
- 'https://user-images.githubusercontent.com/9403740/262918875-603f4adb-58dd-49b5-8201-341d354935fd.png',
- );
},
);
},
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart
index 239e7e09a8e23..018ed1b8d4db5 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_test_runner.dart
@@ -12,10 +12,13 @@ import 'document_more_actions_test.dart' as document_more_actions_test;
import 'document_text_direction_test.dart' as document_text_direction_test;
import 'document_with_cover_image_test.dart' as document_with_cover_image_test;
import 'document_with_database_test.dart' as document_with_database_test;
+import 'document_with_file_test.dart' as document_with_file_test;
import 'document_with_image_block_test.dart' as document_with_image_block_test;
import 'document_with_inline_math_equation_test.dart'
as document_with_inline_math_equation_test;
import 'document_with_inline_page_test.dart' as document_with_inline_page_test;
+import 'document_with_multi_image_block_test.dart'
+ as document_with_multi_image_block_test;
import 'document_with_outline_block_test.dart' as document_with_outline_block;
import 'document_with_toggle_list_test.dart' as document_with_toggle_list_test;
import 'edit_document_test.dart' as document_edit_test;
@@ -38,6 +41,8 @@ void startTesting() {
document_text_direction_test.main();
document_option_action_test.main();
document_with_image_block_test.main();
+ document_with_multi_image_block_test.main();
document_inline_page_reference_test.main();
document_more_actions_test.main();
+ document_with_file_test.main();
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart
index f401cb1e0be12..eb07a2e7a84f5 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_database_test.dart
@@ -1,4 +1,3 @@
-import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/board/presentation/board_page.dart';
import 'package:appflowy/plugins/database/calendar/presentation/calendar_page.dart';
import 'package:appflowy/plugins/database/grid/presentation/grid_page.dart';
@@ -7,7 +6,6 @@ import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.d
import 'package:appflowy/workspace/presentation/home/menu/view/view_item.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/uuid.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -22,7 +20,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
- await insertReferenceDatabase(tester, ViewLayoutPB.Grid);
+ await insertLinkedDatabase(tester, ViewLayoutPB.Grid);
// validate the referenced grid is inserted
expect(
@@ -50,7 +48,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
- await insertReferenceDatabase(tester, ViewLayoutPB.Board);
+ await insertLinkedDatabase(tester, ViewLayoutPB.Board);
// validate the referenced board is inserted
expect(
@@ -66,7 +64,7 @@ void main() {
await tester.initializeAppFlowy();
await tester.tapAnonymousSignInButton();
- await insertReferenceDatabase(tester, ViewLayoutPB.Calendar);
+ await insertLinkedDatabase(tester, ViewLayoutPB.Calendar);
// validate the referenced grid is inserted
expect(
@@ -129,7 +127,7 @@ void main() {
}
/// Insert a referenced database of [layout] into the document
-Future insertReferenceDatabase(
+Future insertLinkedDatabase(
WidgetTester tester,
ViewLayoutPB layout,
) async {
@@ -150,7 +148,7 @@ Future insertReferenceDatabase(
// insert a referenced view
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
- layout.referencedMenuName,
+ layout.slashMenuLinkedName,
);
final linkToPageMenu = find.byType(InlineActionsHandler);
@@ -176,16 +174,9 @@ Future createInlineDatabase(
await tester.editor.tapLineOfEditorAt(0);
// insert a referenced view
await tester.editor.showSlashMenu();
- final name = switch (layout) {
- ViewLayoutPB.Grid => LocaleKeys.document_slashMenu_grid_createANewGrid.tr(),
- ViewLayoutPB.Board =>
- LocaleKeys.document_slashMenu_board_createANewBoard.tr(),
- ViewLayoutPB.Calendar =>
- LocaleKeys.document_slashMenu_calendar_createANewCalendar.tr(),
- _ => '',
- };
await tester.editor.tapSlashMenuItemWithName(
- name,
+ layout.slashMenuName,
+ offset: 100,
);
await tester.pumpAndSettle();
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_file_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_file_test.dart
new file mode 100644
index 0000000000000..76ad7d612fb1f
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_file_test.dart
@@ -0,0 +1,169 @@
+import 'dart:io';
+
+import 'package:appflowy/core/config/kv.dart';
+import 'package:appflowy/core/config/kv_keys.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/file/file_upload_menu.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.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/mock/mock_file_picker.dart';
+import '../../shared/util.dart';
+
+void main() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ group('file block in document', () {
+ testWidgets('insert a file from local file + rename file', (tester) async {
+ await tester.initializeAppFlowy();
+ await tester.tapAnonymousSignInButton();
+
+ // create a new document
+ await tester.createNewPageWithNameUnderParent(name: 'Insert file test');
+
+ // tap the first line of the document
+ await tester.editor.tapLineOfEditorAt(0);
+ await tester.editor.showSlashMenu();
+ await tester.editor.tapSlashMenuItemWithName(
+ LocaleKeys.document_slashMenu_name_file.tr(),
+ );
+ expect(find.byType(FileBlockComponent), findsOneWidget);
+
+ await tester.tap(find.byType(FileBlockComponent));
+ await tester.pumpAndSettle(const Duration(seconds: 1));
+
+ expect(find.byType(FileUploadMenu), findsOneWidget);
+
+ final image = await rootBundle.load('assets/test/images/sample.jpeg');
+ final tempDirectory = await getTemporaryDirectory();
+ final filePath = p.join(tempDirectory.path, 'sample.jpeg');
+ final file = File(filePath)..writeAsBytesSync(image.buffer.asUint8List());
+
+ mockPickFilePaths(paths: [filePath]);
+
+ await getIt().set(KVKeys.kCloudType, '0');
+ await tester.tap(
+ find.text(LocaleKeys.document_plugins_file_fileUploadHint.tr()),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.byType(FileUploadMenu), findsNothing);
+ expect(find.byType(FileBlockComponent), findsOneWidget);
+
+ final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
+ expect(node.type, FileBlockKeys.type);
+ expect(node.attributes[FileBlockKeys.url], isNotEmpty);
+ expect(
+ node.attributes[FileBlockKeys.urlType],
+ FileUrlType.local.toIntValue(),
+ );
+
+ // Check the name of the file is correctly extracted
+ expect(node.attributes[FileBlockKeys.name], 'sample.jpeg');
+ expect(find.text('sample.jpeg'), findsOneWidget);
+
+ const newName = "Renamed file";
+
+ // Hover on the widget to see the three dots to open FileBlockMenu
+ await tester.hoverOnWidget(
+ find.byType(FileBlockComponent),
+ onHover: () async {
+ await tester.tap(find.byType(FileMenuTrigger));
+ await tester.pumpAndSettle();
+
+ await tester.tap(
+ find.text(LocaleKeys.document_plugins_file_renameFile_title.tr()),
+ );
+ },
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.byType(FlowyTextField), findsOneWidget);
+ await tester.enterText(find.byType(FlowyTextField), newName);
+ await tester.pump();
+
+ await tester.tap(find.text(LocaleKeys.button_save.tr()));
+ await tester.pumpAndSettle();
+
+ final updatedNode =
+ tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
+ expect(updatedNode.attributes[FileBlockKeys.name], newName);
+ expect(find.text(newName), findsOneWidget);
+
+ // remove the temp file
+ file.deleteSync();
+ });
+
+ testWidgets('insert a file from network', (tester) async {
+ await tester.initializeAppFlowy();
+ await tester.tapAnonymousSignInButton();
+
+ // create a new document
+ await tester.createNewPageWithNameUnderParent(name: 'Insert file test');
+
+ // tap the first line of the document
+ await tester.editor.tapLineOfEditorAt(0);
+ await tester.editor.showSlashMenu();
+ await tester.editor.tapSlashMenuItemWithName(
+ LocaleKeys.document_slashMenu_name_file.tr(),
+ );
+ expect(find.byType(FileBlockComponent), findsOneWidget);
+
+ await tester.tap(find.byType(FileBlockComponent));
+ await tester.pumpAndSettle(const Duration(seconds: 1));
+ expect(find.byType(FileUploadMenu), findsOneWidget);
+
+ // Navigate to integrate link tab
+ await tester.tapButtonWithName(
+ LocaleKeys.document_plugins_file_networkTab.tr(),
+ );
+ await tester.pumpAndSettle();
+
+ const url =
+ 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
+ await tester.enterText(
+ find.descendant(
+ of: find.byType(FileUploadMenu),
+ matching: find.byType(FlowyTextField),
+ ),
+ url,
+ );
+ await tester.tapButton(
+ find.descendant(
+ of: find.byType(FileUploadMenu),
+ matching: find.text(
+ LocaleKeys.document_plugins_file_networkAction.tr(),
+ findRichText: true,
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.byType(FileUploadMenu), findsNothing);
+ expect(find.byType(FileBlockComponent), findsOneWidget);
+
+ final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
+ expect(node.type, FileBlockKeys.type);
+ expect(node.attributes[FileBlockKeys.url], isNotEmpty);
+ expect(
+ node.attributes[FileBlockKeys.urlType],
+ FileUrlType.network.toIntValue(),
+ );
+
+ // Check the name is correctly extracted from the url
+ expect(
+ node.attributes[FileBlockKeys.name],
+ 'photo-1469474968028-56623f02e42e',
+ );
+ expect(find.text('photo-1469474968028-56623f02e42e'), findsOneWidget);
+ });
+ });
+}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart
index 976d812da1165..fb0c68682490a 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_image_block_test.dart
@@ -3,12 +3,12 @@ import 'dart:io';
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component.dart';
-import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/image/custom_image_block_component/custom_image_block_component.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/image_placeholder.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/resizeable_image.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/image/unsplash_image_widget.dart';
-import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu.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;
@@ -36,13 +36,15 @@ void main() {
// create a new document
await tester.createNewPageWithNameUnderParent(
- name: LocaleKeys.document_plugins_image_addAnImage.tr(),
+ name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
- await tester.editor.tapSlashMenuItemWithName('Image');
+ await tester.editor.tapSlashMenuItemWithName(
+ LocaleKeys.document_slashMenu_name_image.tr(),
+ );
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
@@ -84,13 +86,15 @@ void main() {
// create a new document
await tester.createNewPageWithNameUnderParent(
- name: LocaleKeys.document_plugins_image_addAnImage.tr(),
+ name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
- await tester.editor.tapSlashMenuItemWithName('Image');
+ await tester.editor.tapSlashMenuItemWithName(
+ LocaleKeys.document_slashMenu_name_image.tr(),
+ );
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
@@ -137,13 +141,15 @@ void main() {
// create a new document
await tester.createNewPageWithNameUnderParent(
- name: LocaleKeys.document_plugins_image_addAnImage.tr(),
+ name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
);
// tap the first line of the document
await tester.editor.tapLineOfEditorAt(0);
await tester.editor.showSlashMenu();
- await tester.editor.tapSlashMenuItemWithName('Image');
+ await tester.editor.tapSlashMenuItemWithName(
+ LocaleKeys.document_slashMenu_name_image.tr(),
+ );
expect(find.byType(CustomImageBlockComponent), findsOneWidget);
expect(find.byType(ImagePlaceholder), findsOneWidget);
expect(
@@ -161,5 +167,69 @@ void main() {
expect(find.byType(UnsplashImageWidget), findsOneWidget);
});
});
+
+ testWidgets('insert two images from local file at once', (tester) async {
+ await tester.initializeAppFlowy();
+ await tester.tapAnonymousSignInButton();
+
+ // create a new document
+ await tester.createNewPageWithNameUnderParent(
+ name: LocaleKeys.document_plugins_image_addAnImageDesktop.tr(),
+ );
+
+ // tap the first line of the document
+ await tester.editor.tapLineOfEditorAt(0);
+ await tester.editor.showSlashMenu();
+ await tester.editor.tapSlashMenuItemWithName(
+ 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);
+
+ final firstImage =
+ await rootBundle.load('assets/test/images/sample.jpeg');
+ final secondImage =
+ await rootBundle.load('assets/test/images/sample.gif');
+ final tempDirectory = await getTemporaryDirectory();
+
+ final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
+ final firstFile = File(firstImagePath)
+ ..writeAsBytesSync(firstImage.buffer.asUint8List());
+
+ final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
+ final secondFile = File(secondImagePath)
+ ..writeAsBytesSync(secondImage.buffer.asUint8List());
+
+ mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
+
+ await getIt().set(KVKeys.kCloudType, '0');
+ await tester.tapButtonWithName(
+ LocaleKeys.document_imageBlock_upload_placeholder.tr(),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.byType(ResizableImage), findsNWidgets(2));
+
+ final firstNode =
+ tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
+ expect(firstNode.type, ImageBlockKeys.type);
+ expect(firstNode.attributes[ImageBlockKeys.url], isNotEmpty);
+
+ final secondNode =
+ tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
+ expect(secondNode.type, ImageBlockKeys.type);
+ expect(secondNode.attributes[ImageBlockKeys.url], isNotEmpty);
+
+ // remove the temp files
+ await Future.wait([firstFile.delete(), secondFile.delete()]);
+ });
});
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart
index 27f02f17cd8fe..c4a8e71a02b6a 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_math_equation_test.dart
@@ -33,7 +33,7 @@ void main() {
);
// tap the inline math equation button
- final inlineMathEquationButton = find.byTooltip(
+ final inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
@@ -78,7 +78,7 @@ void main() {
);
// tap the inline math equation button
- var inlineMathEquationButton = find.byTooltip(
+ var inlineMathEquationButton = find.findFlowyTooltip(
LocaleKeys.document_plugins_createInlineMathEquation.tr(),
);
await tester.tapButton(inlineMathEquationButton);
@@ -93,11 +93,11 @@ void main() {
);
// expect to the see the inline math equation button is highlighted
- inlineMathEquationButton = find.byWidgetPredicate(
- (widget) =>
- widget is SVGIconItemWidget &&
- widget.tooltip ==
- LocaleKeys.document_plugins_createInlineMathEquation.tr(),
+ inlineMathEquationButton = find.descendant(
+ of: find.findFlowyTooltip(
+ LocaleKeys.document_plugins_createInlineMathEquation.tr(),
+ ),
+ matching: find.byType(SVGIconItemWidget),
);
expect(
tester.widget(inlineMathEquationButton).isHighlight,
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart
index 335f9a377fc21..45613fe97fc96 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_inline_page_test.dart
@@ -1,7 +1,7 @@
import 'package:appflowy/plugins/document/presentation/editor_plugins/mention/mention_page_block.dart';
+import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:flowy_infra/uuid.dart';
-import 'package:flowy_infra_ui/widget/error_page.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
@@ -92,7 +92,7 @@ void main() {
);
expect(finder, findsOneWidget);
await tester.tapButton(finder);
- expect(find.byType(FlowyErrorPage), findsOneWidget);
+ expect(find.byType(AppFlowyErrorPage), findsOneWidget);
});
});
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart
new file mode 100644
index 0000000000000..d85e6c631eccf
--- /dev/null
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_multi_image_block_test.dart
@@ -0,0 +1,293 @@
+import 'dart:io';
+
+import 'package:appflowy/core/config/kv.dart';
+import 'package:appflowy/core/config/kv_keys.dart';
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/image_render.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/layouts/image_browser_layout.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_block_component.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/image/multi_image_block_component/multi_image_placeholder.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/upload_image_menu.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
+import 'package:appflowy/shared/appflowy_network_image.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_toolbar.dart';
+import 'package:appflowy/workspace/presentation/widgets/image_viewer/interactive_image_viewer.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/gestures.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 '../../shared/mock/mock_file_picker.dart';
+import '../../shared/util.dart';
+import '../board/board_hide_groups_test.dart';
+
+void main() {
+ setUp(() {
+ IntegrationTestWidgetsFlutterBinding.ensureInitialized();
+ TestWidgetsFlutterBinding.ensureInitialized();
+ });
+
+ group('multi image block in document', () {
+ testWidgets('insert images from local and use interactive viewer',
+ (tester) async {
+ await tester.initializeAppFlowy();
+ await tester.tapAnonymousSignInButton();
+
+ // create a new document
+ await tester.createNewPageWithNameUnderParent(
+ name: 'multi image block test',
+ );
+
+ // tap the first line of the document
+ await tester.editor.tapLineOfEditorAt(0);
+ await tester.editor.showSlashMenu();
+ await tester.editor.tapSlashMenuItemWithName(
+ LocaleKeys.document_slashMenu_name_photoGallery.tr(),
+ offset: 100,
+ );
+ expect(find.byType(MultiImageBlockComponent), findsOneWidget);
+ expect(find.byType(MultiImagePlaceholder), findsOneWidget);
+
+ await tester.tap(find.byType(MultiImagePlaceholder));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(UploadImageMenu), findsOneWidget);
+
+ final firstImage =
+ await rootBundle.load('assets/test/images/sample.jpeg');
+ final secondImage =
+ await rootBundle.load('assets/test/images/sample.gif');
+ final tempDirectory = await getTemporaryDirectory();
+
+ final firstImagePath = p.join(tempDirectory.path, 'sample.jpeg');
+ final firstFile = File(firstImagePath)
+ ..writeAsBytesSync(firstImage.buffer.asUint8List());
+
+ final secondImagePath = p.join(tempDirectory.path, 'sample.gif');
+ final secondFile = File(secondImagePath)
+ ..writeAsBytesSync(secondImage.buffer.asUint8List());
+
+ mockPickFilePaths(paths: [firstImagePath, secondImagePath]);
+
+ await getIt().set(KVKeys.kCloudType, '0');
+ await tester.tapButtonWithName(
+ LocaleKeys.document_imageBlock_upload_placeholder.tr(),
+ );
+ await tester.pumpAndSettle();
+ expect(find.byType(ImageBrowserLayout), findsOneWidget);
+ final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
+ expect(node.type, MultiImageBlockKeys.type);
+
+ final data = MultiImageData.fromJson(
+ node.attributes[MultiImageBlockKeys.images],
+ );
+
+ expect(data.images.length, 2);
+
+ // Start using the interactive viewer to view the image(s)
+ final imageFinder = find
+ .byWidgetPredicate(
+ (w) =>
+ w is Image &&
+ w.image is FileImage &&
+ (w.image as FileImage).file.path.endsWith('.jpeg'),
+ )
+ .first;
+ await tester.tap(imageFinder);
+ await tester.pump(kDoubleTapMinTime);
+ await tester.tap(imageFinder);
+ await tester.pumpAndSettle();
+
+ final ivFinder = find.byType(InteractiveImageViewer);
+ expect(ivFinder, findsOneWidget);
+
+ // go to next image
+ await tester.tap(find.byFlowySvg(FlowySvgs.arrow_right_s));
+ await tester.pumpAndSettle();
+
+ // Expect image to end with .gif
+ final gifImageFinder = find.byWidgetPredicate(
+ (w) =>
+ w is Image &&
+ w.image is FileImage &&
+ (w.image as FileImage).file.path.endsWith('.gif'),
+ );
+
+ gifImageFinder.evaluate();
+ expect(gifImageFinder.found.length, 2);
+
+ // go to previous image
+ await tester.tap(find.byFlowySvg(FlowySvgs.arrow_left_s));
+ await tester.pumpAndSettle();
+
+ gifImageFinder.evaluate();
+ expect(gifImageFinder.found.length, 1);
+
+ // remove the temp files
+ await Future.wait([firstFile.delete(), secondFile.delete()]);
+ });
+
+ testWidgets('insert and delete images from network', (tester) async {
+ await tester.initializeAppFlowy();
+ await tester.tapAnonymousSignInButton();
+
+ // create a new document
+ await tester.createNewPageWithNameUnderParent(
+ name: 'multi image block test',
+ );
+
+ // tap the first line of the document
+ await tester.editor.tapLineOfEditorAt(0);
+ await tester.editor.showSlashMenu();
+ await tester.editor.tapSlashMenuItemWithName(
+ LocaleKeys.document_slashMenu_name_photoGallery.tr(),
+ offset: 100,
+ );
+ expect(find.byType(MultiImageBlockComponent), findsOneWidget);
+ expect(find.byType(MultiImagePlaceholder), findsOneWidget);
+
+ await tester.tap(find.byType(MultiImagePlaceholder));
+ await tester.pumpAndSettle();
+
+ expect(find.byType(UploadImageMenu), findsOneWidget);
+
+ await tester.tapButtonWithName(
+ LocaleKeys.document_imageBlock_embedLink_label.tr(),
+ );
+
+ const url =
+ 'https://images.unsplash.com/photo-1469474968028-56623f02e42e?ixlib=rb-4.0.3&q=85&fm=jpg&crop=entropy&cs=srgb&dl=david-marcu-78A265wPiO4-unsplash.jpg&w=640';
+ await tester.enterText(
+ find.descendant(
+ of: find.byType(EmbedImageUrlWidget),
+ matching: find.byType(TextField),
+ ),
+ url,
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tapButton(
+ find.descendant(
+ of: find.byType(EmbedImageUrlWidget),
+ matching: find.text(
+ LocaleKeys.document_imageBlock_embedLink_label.tr(),
+ findRichText: true,
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.byType(ImageBrowserLayout), findsOneWidget);
+ final node = tester.editor.getCurrentEditorState().getNodeAtPath([0])!;
+ expect(node.type, MultiImageBlockKeys.type);
+
+ final data = MultiImageData.fromJson(
+ node.attributes[MultiImageBlockKeys.images],
+ );
+
+ expect(data.images.length, 1);
+
+ final imageFinder = find
+ .byWidgetPredicate(
+ (w) => w is FlowyNetworkImage && w.url == url,
+ )
+ .first;
+
+ // Insert two images from network
+ for (int i = 0; i < 2; i++) {
+ // Hover on the image to show the image toolbar
+ await tester.hoverOnWidget(
+ imageFinder,
+ onHover: () async {
+ // Click on the add
+ final addFinder = find.descendant(
+ of: find.byType(MultiImageMenu),
+ matching: find.byFlowySvg(FlowySvgs.add_s),
+ );
+
+ expect(addFinder, findsOneWidget);
+ await tester.tap(addFinder);
+ await tester.pumpAndSettle();
+
+ await tester.tapButtonWithName(
+ LocaleKeys.document_imageBlock_embedLink_label.tr(),
+ );
+
+ await tester.enterText(
+ find.descendant(
+ of: find.byType(EmbedImageUrlWidget),
+ matching: find.byType(TextField),
+ ),
+ url,
+ );
+ await tester.pumpAndSettle();
+
+ await tester.tapButton(
+ find.descendant(
+ of: find.byType(EmbedImageUrlWidget),
+ matching: find.text(
+ LocaleKeys.document_imageBlock_embedLink_label.tr(),
+ findRichText: true,
+ ),
+ ),
+ );
+ await tester.pumpAndSettle();
+ },
+ );
+ }
+
+ await tester.pumpAndSettle();
+
+ // There should be 4 images visible now, where 2 are thumbnails
+ expect(find.byType(ThumbnailItem), findsNWidgets(3));
+
+ // And all three use ImageRender
+ expect(find.byType(ImageRender), findsNWidgets(4));
+
+ // Hover on and delete the first thumbnail image
+ await tester.hoverOnWidget(find.byType(ThumbnailItem).first);
+
+ final deleteFinder = find
+ .descendant(
+ of: find.byType(ThumbnailItem),
+ matching: find.byFlowySvg(FlowySvgs.delete_s),
+ )
+ .first;
+
+ expect(deleteFinder, findsOneWidget);
+ await tester.tap(deleteFinder);
+ await tester.pumpAndSettle();
+
+ expect(find.byType(ImageRender), findsNWidgets(3));
+
+ // Delete one from interactive viewer
+ await tester.tap(imageFinder);
+ await tester.pump(kDoubleTapMinTime);
+ await tester.tap(imageFinder);
+ await tester.pumpAndSettle();
+
+ final ivFinder = find.byType(InteractiveImageViewer);
+ expect(ivFinder, findsOneWidget);
+
+ await tester.tap(
+ find.descendant(
+ of: find.byType(InteractiveImageToolbar),
+ matching: find.byFlowySvg(FlowySvgs.delete_s),
+ ),
+ );
+ await tester.pumpAndSettle();
+
+ expect(find.byType(InteractiveImageViewer), findsNothing);
+
+ // There should be 1 image and the thumbnail for said image still visible
+ expect(find.byType(ImageRender), findsNWidgets(2));
+ });
+ });
+}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart
index bfd8198295d84..fc6d0f86a6fbf 100644
--- a/frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart
+++ b/frontend/appflowy_flutter/integration_test/desktop/document/document_with_outline_block_test.dart
@@ -171,7 +171,8 @@ Future insertOutlineInDocument(WidgetTester tester) async {
// open the actions menu and insert the outline block
await tester.editor.showSlashMenu();
await tester.editor.tapSlashMenuItemWithName(
- LocaleKeys.document_selectionMenu_outline.tr(),
+ LocaleKeys.document_slashMenu_name_outline.tr(),
+ offset: 100,
);
await tester.pumpAndSettle();
}
diff --git a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart b/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart
deleted file mode 100644
index 1d42cd8d280b4..0000000000000
--- a/frontend/appflowy_flutter/integration_test/desktop/uncategorized/open_ai_smart_menu_test.dart
+++ /dev/null
@@ -1,113 +0,0 @@
-import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/service/ai_client.dart';
-import 'package:appflowy/startup/startup.dart';
-import 'package:appflowy_editor/appflowy_editor.dart';
-import 'package:appflowy_editor/src/render/toolbar/toolbar_widget.dart';
-import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
-import 'package:integration_test/integration_test.dart';
-
-import '../../shared/mock/mock_openai_repository.dart';
-import '../../shared/util.dart';
-
-void main() {
- IntegrationTestWidgetsFlutterBinding.ensureInitialized();
- const service = TestWorkspaceService(TestWorkspace.aiWorkSpace);
-
- group('integration tests for open-ai smart menu', () {
- setUpAll(() async => service.setUpAll());
- setUp(() async => service.setUp());
-
- testWidgets('testing selection on open-ai smart menu replace',
- (tester) async {
- final appFlowyEditor = await setUpOpenAITesting(tester);
- final editorState = appFlowyEditor.editorState;
-
- editorState.service.selectionService.updateSelection(
- Selection(
- start: Position(path: [1], offset: 4),
- end: Position(path: [1], offset: 10),
- ),
- );
- await tester.pumpAndSettle(const Duration(milliseconds: 500));
- await tester.pumpAndSettle();
-
- expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
-
- await tester.tap(find.byTooltip('AI Assistants'));
- await tester.pumpAndSettle(const Duration(milliseconds: 500));
-
- await tester.tap(find.text('Summarize'));
- await tester.pumpAndSettle();
-
- await tester
- .tap(find.byType(FlowyRichTextButton, skipOffstage: false).first);
- await tester.pumpAndSettle();
-
- expect(
- editorState.service.selectionService.currentSelection.value,
- Selection(
- start: Position(path: [1], offset: 4),
- end: Position(path: [1], offset: 84),
- ),
- );
- });
- testWidgets('testing selection on open-ai smart menu insert',
- (tester) async {
- final appFlowyEditor = await setUpOpenAITesting(tester);
- final editorState = appFlowyEditor.editorState;
-
- editorState.service.selectionService.updateSelection(
- Selection(
- start: Position(path: [1]),
- end: Position(path: [1], offset: 5),
- ),
- );
- await tester.pumpAndSettle(const Duration(milliseconds: 500));
- await tester.pumpAndSettle();
- expect(find.byType(ToolbarWidget), findsAtLeastNWidgets(1));
-
- await tester.tap(find.byTooltip('AI Assistants'));
- await tester.pumpAndSettle(const Duration(milliseconds: 500));
-
- await tester.tap(find.text('Summarize'));
- await tester.pumpAndSettle();
-
- await tester
- .tap(find.byType(FlowyRichTextButton, skipOffstage: false).at(1));
- await tester.pumpAndSettle();
-
- expect(
- editorState.service.selectionService.currentSelection.value,
- Selection(
- start: Position(path: [2]),
- end: Position(path: [3]),
- ),
- );
- });
- });
-}
-
-Future setUpOpenAITesting(WidgetTester tester) async {
- await tester.initializeAppFlowy();
- await mockOpenAIRepository();
-
- await simulateKeyDownEvent(LogicalKeyboardKey.controlLeft);
- await simulateKeyDownEvent(LogicalKeyboardKey.backslash);
- await tester.pumpAndSettle();
-
- final Finder editor = find.byType(AppFlowyEditor);
- await tester.tap(editor);
- await tester.pumpAndSettle();
- return tester.state(editor).widget as AppFlowyEditor;
-}
-
-Future mockOpenAIRepository() async {
- await getIt.unregister();
- getIt.registerFactoryAsync(
- () => Future.value(
- MockOpenAIRepository(),
- ),
- );
- return;
-}
diff --git a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart
index e2c926fcfebf6..1f2f23dc2c96b 100644
--- a/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart
+++ b/frontend/appflowy_flutter/integration_test/shared/auth_operation.dart
@@ -1,12 +1,10 @@
-import 'package:flutter/material.dart';
-
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
-import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
+import 'package:appflowy/workspace/presentation/settings/pages/account/account.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/setting_appflowy_cloud.dart';
-import 'package:appflowy/workspace/presentation/settings/widgets/setting_supabase_cloud.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'util.dart';
@@ -14,7 +12,7 @@ import 'util.dart';
extension AppFlowyAuthTest on WidgetTester {
Future tapGoogleLoginInButton() async {
await tapButton(
- find.byKey(const Key('signInWithGoogleButton')),
+ find.byKey(signInWithGoogleButtonKey),
);
}
@@ -22,15 +20,15 @@ extension AppFlowyAuthTest on WidgetTester {
Future logout() async {
final scrollable = find.findSettingsScrollable();
await scrollUntilVisible(
- find.byType(SignInOutButton),
+ find.byType(AccountSignInOutButton),
100,
scrollable: scrollable,
);
- await tapButton(find.byType(SignInOutButton));
+ await tapButton(find.byType(AccountSignInOutButton));
- expectToSeeText(LocaleKeys.button_confirm.tr());
- await tapButtonWithName(LocaleKeys.button_confirm.tr());
+ expectToSeeText(LocaleKeys.button_ok.tr());
+ await tapButtonWithName(LocaleKeys.button_ok.tr());
}
Future tapSignInAsGuest() async {
@@ -38,7 +36,7 @@ extension AppFlowyAuthTest on WidgetTester {
}
void expectToSeeGoogleLoginButton() {
- expect(find.byKey(const Key('signInWithGoogleButton')), findsOneWidget);
+ expect(find.byKey(signInWithGoogleButtonKey), findsOneWidget);
}
void assertSwitchValue(Finder finder, bool value) {
@@ -53,26 +51,6 @@ extension AppFlowyAuthTest on WidgetTester {
assert(isSwitched == value);
}
- void assertEnableEncryptSwitchValue(bool value) {
- assertSwitchValue(
- find.descendant(
- of: find.byType(EnableEncrypt),
- matching: find.byWidgetPredicate((widget) => widget is Switch),
- ),
- value,
- );
- }
-
- void assertSupabaseEnableSyncSwitchValue(bool value) {
- assertSwitchValue(
- find.descendant(
- of: find.byType(SupabaseEnableSync),
- matching: find.byWidgetPredicate((widget) => widget is Switch),
- ),
- value,
- );
- }
-
void assertAppFlowyCloudEnableSyncSwitchValue(bool value) {
assertToggleValue(
find.descendant(
@@ -83,15 +61,6 @@ extension AppFlowyAuthTest on WidgetTester {
);
}
- Future toggleEnableEncrypt() async {
- final finder = find.descendant(
- of: find.byType(EnableEncrypt),
- matching: find.byWidgetPredicate((widget) => widget is Switch),
- );
-
- await tapButton(finder);
- }
-
Future toggleEnableSync(Type syncButton) async {
final finder = find.descendant(
of: find.byType(syncButton),
diff --git a/frontend/appflowy_flutter/integration_test/shared/base.dart b/frontend/appflowy_flutter/integration_test/shared/base.dart
index ab72247c24a2e..371cd9b839ae6 100644
--- a/frontend/appflowy_flutter/integration_test/shared/base.dart
+++ b/frontend/appflowy_flutter/integration_test/shared/base.dart
@@ -1,21 +1,19 @@
import 'dart:async';
import 'dart:io';
-import 'package:flutter/gestures.dart';
-import 'package:flutter/services.dart';
-
import 'package:appflowy/env/cloud_env.dart';
import 'package:appflowy/env/cloud_env_test.dart';
import 'package:appflowy/startup/entry_point.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/af_cloud_mock_auth_service.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
-import 'package:appflowy/user/application/auth/supabase_mock_auth_service.dart';
import 'package:appflowy/user/presentation/presentation.dart';
import 'package:appflowy/user/presentation/screens/sign_in_screen/widgets/widgets.dart';
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/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@@ -56,8 +54,6 @@ extension AppFlowyTestBase on WidgetTester {
switch (cloudType) {
case AuthenticatorType.local:
break;
- case AuthenticatorType.supabase:
- break;
case AuthenticatorType.appflowyCloudSelfHost:
rustEnvs["GOTRUE_ADMIN_EMAIL"] = "admin@example.com";
rustEnvs["GOTRUE_ADMIN_PASSWORD"] = "password";
@@ -76,13 +72,6 @@ extension AppFlowyTestBase on WidgetTester {
case AuthenticatorType.local:
await useLocalServer();
break;
- case AuthenticatorType.supabase:
- await useTestSupabaseCloud();
- getIt.unregister();
- getIt.registerFactory(
- () => SupabaseMockAuthService(),
- );
- break;
case AuthenticatorType.appflowyCloudSelfHost:
await useTestSelfHostedAppFlowyCloud();
getIt.unregister();
@@ -231,13 +220,16 @@ extension AppFlowyFinderTestBase on CommonFinders {
(widget) => widget is FlowyText && widget.text == text,
);
}
-}
-Future useTestSupabaseCloud() async {
- await useSupabaseCloud(
- url: TestEnv.supabaseUrl,
- anonKey: TestEnv.supabaseAnonKey,
- );
+ Finder findFlowyTooltip(String richMessage, {bool skipOffstage = true}) {
+ return byWidgetPredicate(
+ (widget) =>
+ widget is FlowyTooltip &&
+ widget.richMessage != null &&
+ widget.richMessage!.toPlainText().contains(richMessage),
+ skipOffstage: skipOffstage,
+ );
+ }
}
Future useTestSelfHostedAppFlowyCloud() async {
diff --git a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart
index 8af2b2f075e7d..33ae6a1eca355 100644
--- a/frontend/appflowy_flutter/integration_test/shared/common_operations.dart
+++ b/frontend/appflowy_flutter/integration_test/shared/common_operations.dart
@@ -1,5 +1,10 @@
import 'dart:io';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/gestures.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+
import 'package:appflowy/core/config/kv.dart';
import 'package:appflowy/core/config/kv_keys.dart';
import 'package:appflowy/generated/flowy_svgs.g.dart';
@@ -30,10 +35,6 @@ import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/widget/buttons/primary_button.dart';
-import 'package:flutter/foundation.dart';
-import 'package:flutter/gestures.dart';
-import 'package:flutter/material.dart';
-import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'emoji.dart';
@@ -671,4 +672,34 @@ extension ViewLayoutPBTest on ViewLayoutPB {
throw UnsupportedError('Unsupported layout: $this');
}
}
+
+ String get slashMenuName {
+ switch (this) {
+ case ViewLayoutPB.Grid:
+ return LocaleKeys.document_slashMenu_name_grid.tr();
+ case ViewLayoutPB.Board:
+ return LocaleKeys.document_slashMenu_name_kanban.tr();
+ case ViewLayoutPB.Document:
+ return LocaleKeys.document_slashMenu_name_doc.tr();
+ case ViewLayoutPB.Calendar:
+ return LocaleKeys.document_slashMenu_name_calendar.tr();
+ default:
+ throw UnsupportedError('Unsupported layout: $this');
+ }
+ }
+
+ String get slashMenuLinkedName {
+ switch (this) {
+ case ViewLayoutPB.Grid:
+ return LocaleKeys.document_slashMenu_name_linkedGrid.tr();
+ case ViewLayoutPB.Board:
+ return LocaleKeys.document_slashMenu_name_linkedKanban.tr();
+ case ViewLayoutPB.Document:
+ return LocaleKeys.document_slashMenu_name_linkedDoc.tr();
+ case ViewLayoutPB.Calendar:
+ return LocaleKeys.document_slashMenu_name_linkedCalendar.tr();
+ default:
+ throw UnsupportedError('Unsupported layout: $this');
+ }
+ }
}
diff --git a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart
index 754e80342cffe..b25fb056a5866 100644
--- a/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart
+++ b/frontend/appflowy_flutter/integration_test/shared/database_test_op.dart
@@ -41,6 +41,7 @@ import 'package:appflowy/plugins/database/tab_bar/desktop/tab_bar_header.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checkbox.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/checklist.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/date.dart';
+import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/media.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/number.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/select_option.dart';
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_skeleton/text.dart';
@@ -50,6 +51,7 @@ import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_cell_edi
import 'package:appflowy/plugins/database/widgets/cell_editor/checklist_progress_bar.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/date_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/extension.dart';
+import 'package:appflowy/plugins/database/widgets/cell_editor/media_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_cell_editor.dart';
import 'package:appflowy/plugins/database/widgets/cell_editor/select_option_text_field.dart';
import 'package:appflowy/plugins/database/widgets/database_layout_ext.dart';
@@ -72,7 +74,6 @@ import 'package:appflowy/util/field_type_extension.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/clear_date_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/date_type_option_button.dart';
import 'package:appflowy/workspace/presentation/widgets/date_picker/widgets/reminder_selector.dart';
-import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy/workspace/presentation/widgets/pop_up_action.dart';
import 'package:appflowy/workspace/presentation/widgets/toggle/toggle.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
@@ -474,7 +475,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
await pumpAndSettle();
if (enter) {
await testTextInput.receiveAction(TextInputAction.done);
- await pumpAndSettle();
+ await pumpAndSettle(const Duration(milliseconds: 500));
} else {
await tapButton(
find.descendant(
@@ -627,12 +628,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
(w) => w is FieldActionCell && w.action == FieldAction.delete,
);
await tapButton(deleteButton);
-
- final confirmButton = find.descendant(
- of: find.byType(NavigatorAlertDialog),
- matching: find.byType(PrimaryTextButton),
- );
- await tapButton(confirmButton);
+ await tapButtonWithName(LocaleKeys.space_delete.tr());
}
Future scrollRowDetailByOffset(Offset offset) async {
@@ -786,7 +782,7 @@ extension AppFlowyDatabaseTest on WidgetTester {
/// Each field has its own cell, so we can find the corresponding cell by
/// the field type after create a new field.
- Future findCellByFieldType(FieldType fieldType) async {
+ void findCellByFieldType(FieldType fieldType) {
final finder = finderForFieldType(fieldType);
expect(finder, findsWidgets);
}
@@ -857,6 +853,11 @@ extension AppFlowyDatabaseTest on WidgetTester {
expect(finder, matcher);
}
+ Future findMediaCellEditor(dynamic matcher) async {
+ final finder = find.byType(MediaCellEditor);
+ expect(finder, matcher);
+ }
+
Future findSelectOptionEditor(dynamic matcher) async {
final finder = find.byType(SelectOptionCellEditor);
expect(finder, matcher);
@@ -887,18 +888,19 @@ extension AppFlowyDatabaseTest on WidgetTester {
}
Future createField(
- FieldType fieldType,
- String name, {
+ FieldType fieldType, {
+ String? name,
ViewLayoutPB layout = ViewLayoutPB.Grid,
}) async {
if (layout == ViewLayoutPB.Grid) {
await scrollToRight(find.byType(GridPage));
}
await tapNewPropertyButton();
- await renameField(name);
+ if (name != null) {
+ await renameField(name);
+ }
await tapSwitchFieldTypeButton();
await selectFieldType(fieldType);
- await dismissFieldEditor();
}
Future tapDatabaseSettingButton() async {
@@ -1580,6 +1582,8 @@ Finder finderForFieldType(FieldType fieldType) {
return find.byType(EditableTextCell, skipOffstage: false);
case FieldType.URL:
return find.byType(EditableURLCell, skipOffstage: false);
+ case FieldType.Media:
+ return find.byType(EditableMediaCell, skipOffstage: false);
default:
throw Exception('Unknown field type: $fieldType');
}
diff --git a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart
index 4eff62321a4b7..388b685bfbdad 100644
--- a/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart
+++ b/frontend/appflowy_flutter/integration_test/shared/editor_test_operations.dart
@@ -3,15 +3,15 @@ import 'dart:ui';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker.dart';
-import 'package:appflowy/plugins/base/emoji/emoji_skin_tone.dart';
-import 'package:appflowy/plugins/base/icon/icon_picker.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_add_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/actions/block_action_option_button.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/cover_editor.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/document_header_node_widget.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/header/emoji_icon_widget.dart';
-import 'package:appflowy/plugins/document/presentation/editor_plugins/image/embed_image_url_widget.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/image/upload_image_menu/widgets/embed_image_url_widget.dart';
import 'package:appflowy/plugins/inline_actions/widgets/inline_actions_handler.dart';
+import 'package:appflowy/shared/icon_emoji_picker/emoji_skin_tone.dart';
+import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
@@ -84,7 +84,7 @@ class EditorOperations {
final Finder button = !isInPicker
? find.text(LocaleKeys.document_plugins_cover_removeIcon.tr())
: find.descendant(
- of: find.byType(FlowyIconPicker),
+ of: find.byType(FlowyIconEmojiPicker),
matching: find.text(LocaleKeys.button_remove.tr()),
);
await tester.tapButton(button);
@@ -170,8 +170,26 @@ class EditorOperations {
/// Tap the slash menu item with [name]
///
/// Must call [showSlashMenu] first.
- Future tapSlashMenuItemWithName(String name) async {
+ Future tapSlashMenuItemWithName(
+ String name, {
+ double offset = 200,
+ }) async {
+ final slashMenu = find
+ .ancestor(
+ of: find.byType(SelectionMenuItemWidget),
+ matching: find.byWidgetPredicate(
+ (widget) => widget is Scrollable,
+ ),
+ )
+ .first;
final slashMenuItem = find.text(name, findRichText: true);
+ await tester.scrollUntilVisible(
+ slashMenuItem,
+ offset,
+ scrollable: slashMenu,
+ duration: const Duration(milliseconds: 250),
+ );
+ assert(slashMenuItem.hasFound);
await tester.tapButton(slashMenuItem);
}
diff --git a/frontend/appflowy_flutter/integration_test/shared/settings.dart b/frontend/appflowy_flutter/integration_test/shared/settings.dart
index a650bd33addce..cb95471cbef91 100644
--- a/frontend/appflowy_flutter/integration_test/shared/settings.dart
+++ b/frontend/appflowy_flutter/integration_test/shared/settings.dart
@@ -3,7 +3,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy/workspace/application/settings/prelude.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/shared/sidebar_setting.dart';
-import 'package:appflowy/workspace/presentation/settings/pages/settings_account_view.dart';
+import 'package:appflowy/workspace/presentation/settings/pages/account/account_user_profile.dart';
import 'package:appflowy/workspace/presentation/settings/pages/settings_workspace_view.dart';
import 'package:appflowy/workspace/presentation/settings/settings_dialog.dart';
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
@@ -13,17 +13,23 @@ import 'package:flowy_infra_ui/style_widget/text_field.dart';
import 'package:flutter_test/flutter_test.dart';
import '../desktop/board/board_hide_groups_test.dart';
-
import 'base.dart';
import 'common_operations.dart';
extension AppFlowySettings on WidgetTester {
/// Open settings page
Future openSettings() async {
+ final settingsDialog = find.byType(SettingsDialog);
+ // tap empty area to close the settings page
+ while (settingsDialog.evaluate().isNotEmpty) {
+ await tapAt(Offset.zero);
+ await pumpAndSettle();
+ }
+
final settingsButton = find.byType(UserSettingButton);
expect(settingsButton, findsOneWidget);
await tapButton(settingsButton);
- final settingsDialog = find.byType(SettingsDialog);
+
expect(settingsDialog, findsOneWidget);
return;
}
@@ -72,14 +78,14 @@ extension AppFlowySettings on WidgetTester {
Future enterUserName(String name) async {
// Enable editing username
final editUsernameFinder = find.descendant(
- of: find.byType(UserProfileSetting),
+ of: find.byType(AccountUserProfile),
matching: find.byFlowySvg(FlowySvgs.edit_s),
);
- await tap(editUsernameFinder);
+ await tap(editUsernameFinder, warnIfMissed: false);
await pumpAndSettle();
final userNameFinder = find.descendant(
- of: find.byType(UserProfileSetting),
+ of: find.byType(AccountUserProfile),
matching: find.byType(FlowyTextField),
);
await enterText(userNameFinder, name);
diff --git a/frontend/appflowy_flutter/integration_test/shared/workspace.dart b/frontend/appflowy_flutter/integration_test/shared/workspace.dart
index 4d20d88ce193f..67506879d59d3 100644
--- a/frontend/appflowy_flutter/integration_test/shared/workspace.dart
+++ b/frontend/appflowy_flutter/integration_test/shared/workspace.dart
@@ -1,5 +1,5 @@
import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/plugins/base/icon/icon_picker.dart';
+import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.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_icon.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/workspace/_sidebar_workspace_menu.dart';
@@ -58,7 +58,7 @@ extension AppFlowyWorkspace on WidgetTester {
);
expect(iconButton, findsOneWidget);
await tapButton(iconButton);
- final iconPicker = find.byType(FlowyIconPicker);
+ final iconPicker = find.byType(FlowyIconEmojiPicker);
expect(iconPicker, findsOneWidget);
await tapButton(find.findTextInFlowyText(icon));
}
diff --git a/frontend/appflowy_flutter/ios/Podfile.lock b/frontend/appflowy_flutter/ios/Podfile.lock
index 86cefebb343b1..28d37bfa2331c 100644
--- a/frontend/appflowy_flutter/ios/Podfile.lock
+++ b/frontend/appflowy_flutter/ios/Podfile.lock
@@ -48,8 +48,6 @@ PODS:
- fluttertoast (0.0.2):
- Flutter
- Toast
- - image_gallery_saver (2.0.2):
- - Flutter
- image_picker_ios (0.0.1):
- Flutter
- integration_test (0.0.1):
@@ -65,12 +63,15 @@ PODS:
- FlutterMacOS
- permission_handler_apple (9.3.0):
- Flutter
- - printing (1.0.0):
- - Flutter
- ReachabilitySwift (5.0.0)
- SDWebImage (5.14.2):
- SDWebImage/Core (= 5.14.2)
- SDWebImage/Core (5.14.2)
+ - Sentry/HybridSDK (8.35.1)
+ - sentry_flutter (8.8.0):
+ - Flutter
+ - FlutterMacOS
+ - Sentry/HybridSDK (= 8.35.1)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -95,7 +96,6 @@ DEPENDENCIES:
- flowy_infra_ui (from `.symlinks/plugins/flowy_infra_ui/ios`)
- Flutter (from `Flutter`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- - image_gallery_saver (from `.symlinks/plugins/image_gallery_saver/ios`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- irondash_engine_context (from `.symlinks/plugins/irondash_engine_context/ios`)
@@ -103,7 +103,7 @@ DEPENDENCIES:
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- - printing (from `.symlinks/plugins/printing/ios`)
+ - sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@@ -116,6 +116,7 @@ SPEC REPOS:
- DKPhotoGallery
- ReachabilitySwift
- SDWebImage
+ - Sentry
- SwiftyGif
- Toast
@@ -136,8 +137,6 @@ EXTERNAL SOURCES:
:path: Flutter
fluttertoast:
:path: ".symlinks/plugins/fluttertoast/ios"
- image_gallery_saver:
- :path: ".symlinks/plugins/image_gallery_saver/ios"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
integration_test:
@@ -152,8 +151,8 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
- printing:
- :path: ".symlinks/plugins/printing/ios"
+ sentry_flutter:
+ :path: ".symlinks/plugins/sentry_flutter/ios"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -175,8 +174,7 @@ SPEC CHECKSUMS:
file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655
flowy_infra_ui: 0455e1fa8c51885aa1437848e361e99419f34ebc
Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
- fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265
- image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb
+ fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
image_picker_ios: 99dfe1854b4fa34d0364e74a78448a0151025425
integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4
irondash_engine_context: 3458bf979b90d616ffb8ae03a150bafe2e860cc9
@@ -184,9 +182,10 @@ SPEC CHECKSUMS:
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2
- printing: 233e1b73bd1f4a05615548e9b5a324c98588640b
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SDWebImage: b9a731e1d6307f44ca703b3976d18c24ca561e84
+ Sentry: 1fe34e9c2cbba1e347623610d26db121dcb569f1
+ sentry_flutter: a39c2a2d67d5e5b9cb0b94a4985c76dd5b3fc737
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
diff --git a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
index aa53cf9b8862f..804ad052be50a 100644
--- a/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
+++ b/frontend/appflowy_flutter/ios/Runner.xcodeproj/project.pbxproj
@@ -372,6 +372,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AppFlowy;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -383,6 +384,8 @@
STRIP_STYLE = "non-global";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
@@ -511,6 +514,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AppFlowy;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -522,6 +526,8 @@
STRIP_STYLE = "non-global";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -545,6 +551,7 @@
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = AppFlowy;
+ INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -556,6 +563,8 @@
STRIP_STYLE = "non-global";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO;
+ SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
+ SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
diff --git a/frontend/appflowy_flutter/ios/Runner/Info.plist b/frontend/appflowy_flutter/ios/Runner/Info.plist
index 5ec528b05e431..0c8c1eff43371 100644
--- a/frontend/appflowy_flutter/ios/Runner/Info.plist
+++ b/frontend/appflowy_flutter/ios/Runner/Info.plist
@@ -1,75 +1,73 @@
-
- NSCameraUsageDescription
- AppFlowy requires access to the camera.
- NSPhotoLibraryUsageDescription
- AppFlowy requires access to the photo library.
- CADisableMinimumFrameDurationOnPhone
-
- CFBundleDevelopmentRegion
- $(DEVELOPMENT_LANGUAGE)
- CFBundleExecutable
- $(EXECUTABLE_NAME)
- CFBundleIdentifier
- $(PRODUCT_BUNDLE_IDENTIFIER)
- CFBundleInfoDictionaryVersion
- 6.0
- CFBundleLocalizations
-
- en
-
- FLTEnableImpeller
-
- CFBundleName
- AppFlowy
- CFBundlePackageType
- APPL
- CFBundleShortVersionString
- $(FLUTTER_BUILD_NAME)
- CFBundleSignature
- ????
- CFBundleURLTypes
-
-
- CFBundleURLName
-
- CFBundleURLSchemes
-
- appflowy-flutter
-
-
-
- CFBundleVersion
- $(FLUTTER_BUILD_NUMBER)
- LSRequiresIPhoneOS
-
- UIApplicationSupportsIndirectInputEvents
-
- UILaunchStoryboardName
- LaunchScreen
- UIMainStoryboardFile
- Main
- UISupportedInterfaceOrientations
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UISupportedInterfaceOrientations~ipad
-
- UIInterfaceOrientationPortrait
- UIInterfaceOrientationPortraitUpsideDown
- UIInterfaceOrientationLandscapeLeft
- UIInterfaceOrientationLandscapeRight
-
- UIViewControllerBasedStatusBarAppearance
-
- NSAppTransportSecurity
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleLocalizations
+
+ en
+
+ CFBundleName
+ AppFlowy
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleURLTypes
+
- NSAllowsArbitraryLoads
-
+ CFBundleURLName
+
+ CFBundleURLSchemes
+
+ appflowy-flutter
+
+
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ FLTEnableImpeller
+
+ LSRequiresIPhoneOS
+
+ NSAppTransportSecurity
+
+ NSAllowsArbitraryLoads
+
-
\ No newline at end of file
+ NSPhotoLibraryUsageDescription
+ AppFlowy needs access to your photos to let you add images to your documents
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+
+ UIViewControllerBasedStatusBarAppearance
+
+ UISupportsDocumentBrowser
+
+
+
diff --git a/frontend/appflowy_flutter/ios/Runner/Runner.entitlements b/frontend/appflowy_flutter/ios/Runner/Runner.entitlements
index 903def2af5306..80b5221de760c 100644
--- a/frontend/appflowy_flutter/ios/Runner/Runner.entitlements
+++ b/frontend/appflowy_flutter/ios/Runner/Runner.entitlements
@@ -4,5 +4,9 @@
aps-environment
development
+ com.apple.developer.applesignin
+
+ Default
+
diff --git a/frontend/appflowy_flutter/lib/core/frameless_window.dart b/frontend/appflowy_flutter/lib/core/frameless_window.dart
index 799c90f5b75b4..15732f163dfa6 100644
--- a/frontend/appflowy_flutter/lib/core/frameless_window.dart
+++ b/frontend/appflowy_flutter/lib/core/frameless_window.dart
@@ -3,16 +3,6 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
-import 'package:appflowy/generated/flowy_svgs.g.dart';
-import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/shared/window_title_bar.dart';
-import 'package:appflowy/util/theme_extension.dart';
-import 'package:appflowy/workspace/application/home/home_setting_bloc.dart';
-import 'package:easy_localization/easy_localization.dart';
-import 'package:flowy_infra_ui/style_widget/hover.dart';
-import 'package:flowy_infra_ui/widget/flowy_tooltip.dart';
-import 'package:flutter_bloc/flutter_bloc.dart';
-
class CocoaWindowChannel {
CocoaWindowChannel._();
@@ -40,11 +30,9 @@ class MoveWindowDetector extends StatefulWidget {
const MoveWindowDetector({
super.key,
this.child,
- this.showTitleBar = false,
});
final Widget? child;
- final bool showTitleBar;
@override
MoveWindowDetectorState createState() => MoveWindowDetectorState();
@@ -56,28 +44,10 @@ class MoveWindowDetectorState extends State {
@override
Widget build(BuildContext context) {
- if (!Platform.isMacOS && !Platform.isWindows) {
+ if (!Platform.isMacOS) {
return widget.child ?? const SizedBox.shrink();
}
- if (Platform.isWindows) {
- return Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (widget.showTitleBar) ...[
- WindowTitleBar(
- leftChildren: [
- _buildToggleMenuButton(context),
- ],
- ),
- ] else ...[
- const SizedBox(height: 5),
- ],
- widget.child ?? const SizedBox.shrink(),
- ],
- );
- }
-
return GestureDetector(
// https://stackoverflow.com/questions/52965799/flutter-gesturedetector-not-working-with-containers-in-stack
behavior: HitTestBehavior.translucent,
@@ -98,47 +68,4 @@ class MoveWindowDetectorState extends State {
child: widget.child,
);
}
-
- Widget _buildToggleMenuButton(BuildContext context) {
- if (!context.read().state.isMenuCollapsed) {
- return const SizedBox.shrink();
- }
-
- final color = Theme.of(context).isLightMode ? Colors.white : Colors.black;
- final textSpan = TextSpan(
- children: [
- TextSpan(
- text: '${LocaleKeys.sideBar_openSidebar.tr()}\n',
- style: Theme.of(context).textTheme.bodyMedium!.copyWith(color: color),
- ),
- TextSpan(
- text: Platform.isMacOS ? '⌘+.' : 'Ctrl+\\',
- style: Theme.of(context)
- .textTheme
- .bodyMedium!
- .copyWith(color: Theme.of(context).hintColor),
- ),
- ],
- );
-
- return FlowyTooltip(
- richMessage: textSpan,
- child: Listener(
- behavior: HitTestBehavior.translucent,
- onPointerDown: (_) => context
- .read()
- .add(const HomeSettingEvent.collapseMenu()),
- child: FlowyHover(
- child: Container(
- width: 24,
- padding: const EdgeInsets.all(4),
- child: const RotatedBox(
- quarterTurns: 2,
- child: FlowySvg(FlowySvgs.hide_menu_s),
- ),
- ),
- ),
- ),
- );
- }
}
diff --git a/frontend/appflowy_flutter/lib/env/backend_env.dart b/frontend/appflowy_flutter/lib/env/backend_env.dart
index fa0bf575a34b7..f8aa715a4062c 100644
--- a/frontend/appflowy_flutter/lib/env/backend_env.dart
+++ b/frontend/appflowy_flutter/lib/env/backend_env.dart
@@ -13,7 +13,6 @@ class AppFlowyConfiguration {
required this.device_id,
required this.platform,
required this.authenticator_type,
- required this.supabase_config,
required this.appflowy_cloud_config,
required this.envs,
});
@@ -28,41 +27,12 @@ class AppFlowyConfiguration {
final String device_id;
final String platform;
final int authenticator_type;
- final SupabaseConfiguration supabase_config;
final AppFlowyCloudConfiguration appflowy_cloud_config;
final Map envs;
Map toJson() => _$AppFlowyConfigurationToJson(this);
}
-@JsonSerializable()
-class SupabaseConfiguration {
- SupabaseConfiguration({
- required this.url,
- required this.anon_key,
- });
-
- factory SupabaseConfiguration.fromJson(Map json) =>
- _$SupabaseConfigurationFromJson(json);
-
- /// Indicates whether the sync feature is enabled.
- final String url;
- final String anon_key;
-
- Map toJson() => _$SupabaseConfigurationToJson(this);
-
- static SupabaseConfiguration defaultConfig() {
- return SupabaseConfiguration(
- url: '',
- anon_key: '',
- );
- }
-
- bool get isValid {
- return url.isNotEmpty && anon_key.isNotEmpty;
- }
-}
-
@JsonSerializable()
class AppFlowyCloudConfiguration {
AppFlowyCloudConfiguration({
diff --git a/frontend/appflowy_flutter/lib/env/cloud_env.dart b/frontend/appflowy_flutter/lib/env/cloud_env.dart
index 9e8ea0d4f93b1..fcad1a1f2f9f6 100644
--- a/frontend/appflowy_flutter/lib/env/cloud_env.dart
+++ b/frontend/appflowy_flutter/lib/env/cloud_env.dart
@@ -21,9 +21,6 @@ Future _setAuthenticatorType(AuthenticatorType ty) async {
case AuthenticatorType.local:
await getIt().set(KVKeys.kCloudType, 0.toString());
break;
- case AuthenticatorType.supabase:
- await getIt().set(KVKeys.kCloudType, 1.toString());
- break;
case AuthenticatorType.appflowyCloud:
await getIt().set(KVKeys.kCloudType, 2.toString());
break;
@@ -63,8 +60,6 @@ Future getAuthenticatorType() async {
switch (value ?? "0") {
case "0":
return AuthenticatorType.local;
- case "1":
- return AuthenticatorType.supabase;
case "2":
return AuthenticatorType.appflowyCloud;
case "3":
@@ -93,10 +88,6 @@ Future getAuthenticatorType() async {
/// Returns `false` otherwise.
bool get isAuthEnabled {
final env = getIt();
- if (env.authenticatorType == AuthenticatorType.supabase) {
- return env.supabaseConfig.isValid;
- }
-
if (env.authenticatorType.isAppFlowyCloudEnabled) {
return env.appflowyCloudConfig.isValid;
}
@@ -104,19 +95,6 @@ bool get isAuthEnabled {
return false;
}
-/// Checks if Supabase is enabled.
-///
-/// This getter evaluates if Supabase should be enabled based on the
-/// current integration mode and cloud type setting.
-///
-/// Returns:
-/// A boolean value indicating whether Supabase is enabled. It returns `true`
-/// if the application is in release or develop mode and the current cloud type
-/// is `CloudType.supabase`. Otherwise, it returns `false`.
-bool get isSupabaseEnabled {
- return currentCloudType().isSupabaseEnabled;
-}
-
/// Determines if AppFlowy Cloud is enabled.
bool get isAppFlowyCloudEnabled {
return currentCloudType().isAppFlowyCloudEnabled;
@@ -124,7 +102,6 @@ bool get isAppFlowyCloudEnabled {
enum AuthenticatorType {
local,
- supabase,
appflowyCloud,
appflowyCloudSelfHost,
// The 'appflowyCloudDevelop' type is used for develop purposes only.
@@ -137,14 +114,10 @@ enum AuthenticatorType {
this == AuthenticatorType.appflowyCloudDevelop ||
this == AuthenticatorType.appflowyCloud;
- bool get isSupabaseEnabled => this == AuthenticatorType.supabase;
-
int get value {
switch (this) {
case AuthenticatorType.local:
return 0;
- case AuthenticatorType.supabase:
- return 1;
case AuthenticatorType.appflowyCloud:
return 2;
case AuthenticatorType.appflowyCloudSelfHost:
@@ -158,8 +131,6 @@ enum AuthenticatorType {
switch (value) {
case 0:
return AuthenticatorType.local;
- case 1:
- return AuthenticatorType.supabase;
case 2:
return AuthenticatorType.appflowyCloud;
case 3:
@@ -197,25 +168,15 @@ Future useLocalServer() async {
await _setAuthenticatorType(AuthenticatorType.local);
}
-Future useSupabaseCloud({
- required String url,
- required String anonKey,
-}) async {
- await _setAuthenticatorType(AuthenticatorType.supabase);
- await setSupabaseServer(url, anonKey);
-}
-
/// Use getIt() to get the shared environment.
class AppFlowyCloudSharedEnv {
AppFlowyCloudSharedEnv({
required AuthenticatorType authenticatorType,
required this.appflowyCloudConfig,
- required this.supabaseConfig,
}) : _authenticatorType = authenticatorType;
final AuthenticatorType _authenticatorType;
final AppFlowyCloudConfiguration appflowyCloudConfig;
- final SupabaseConfiguration supabaseConfig;
AuthenticatorType get authenticatorType => _authenticatorType;
@@ -229,10 +190,6 @@ class AppFlowyCloudSharedEnv {
? await getAppFlowyCloudConfig(authenticatorType)
: AppFlowyCloudConfiguration.defaultConfig();
- final supabaseCloudConfig = authenticatorType.isSupabaseEnabled
- ? await getSupabaseCloudConfig()
- : SupabaseConfiguration.defaultConfig();
-
// In the backend, the value '2' represents the use of AppFlowy Cloud. However, in the frontend,
// we distinguish between [AuthenticatorType.appflowyCloudSelfHost] and [AuthenticatorType.appflowyCloud].
// When the cloud type is [AuthenticatorType.appflowyCloudSelfHost] in the frontend, it should be
@@ -244,7 +201,6 @@ class AppFlowyCloudSharedEnv {
return AppFlowyCloudSharedEnv(
authenticatorType: authenticatorType,
appflowyCloudConfig: appflowyCloudConfig,
- supabaseConfig: supabaseCloudConfig,
);
} else {
// Using the cloud settings from the .env file.
@@ -257,7 +213,6 @@ class AppFlowyCloudSharedEnv {
return AppFlowyCloudSharedEnv(
authenticatorType: AuthenticatorType.fromValue(Env.authenticatorType),
appflowyCloudConfig: appflowyCloudConfig,
- supabaseConfig: SupabaseConfiguration.defaultConfig(),
);
}
}
@@ -265,8 +220,7 @@ class AppFlowyCloudSharedEnv {
@override
String toString() {
return 'authenticator: $_authenticatorType\n'
- 'appflowy: ${appflowyCloudConfig.toJson()}\n'
- 'supabase: ${supabaseConfig.toJson()})\n';
+ 'appflowy: ${appflowyCloudConfig.toJson()}\n';
}
}
@@ -354,22 +308,3 @@ Future setSupabaseServer(
await getIt().set(KVKeys.kSupabaseAnonKey, anonKey);
}
}
-
-Future getSupabaseCloudConfig() async {
- final url = await _getSupabaseUrl();
- final anonKey = await _getSupabaseAnonKey();
- return SupabaseConfiguration(
- url: url,
- anon_key: anonKey,
- );
-}
-
-Future _getSupabaseUrl() async {
- final result = await getIt().get(KVKeys.kSupabaseURL);
- return result ?? '';
-}
-
-Future _getSupabaseAnonKey() async {
- final result = await getIt().get(KVKeys.kSupabaseAnonKey);
- return result ?? '';
-}
diff --git a/frontend/appflowy_flutter/lib/env/env.dart b/frontend/appflowy_flutter/lib/env/env.dart
index b861b4cfb87b0..cfd9837944d09 100644
--- a/frontend/appflowy_flutter/lib/env/env.dart
+++ b/frontend/appflowy_flutter/lib/env/env.dart
@@ -36,4 +36,11 @@ abstract class Env {
defaultValue: '',
)
static const String internalBuild = _Env.internalBuild;
+
+ @EnviedField(
+ obfuscate: false,
+ varName: 'SENTRY_DSN',
+ defaultValue: '',
+ )
+ static const String sentryDsn = _Env.sentryDsn;
}
diff --git a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
index 8b9f1e70ff6e4..153ed451bea74 100644
--- a/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
+++ b/frontend/appflowy_flutter/lib/mobile/application/mobile_router.dart
@@ -2,27 +2,41 @@ import 'dart:async';
import 'dart:convert';
import 'package:appflowy/mobile/presentation/chat/mobile_chat_screen.dart';
-import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
-import 'package:flutter/material.dart';
-
import 'package:appflowy/mobile/presentation/database/board/mobile_board_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_calendar_screen.dart';
import 'package:appflowy/mobile/presentation/database/mobile_grid_screen.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
+import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
extension MobileRouter on BuildContext {
- Future pushView(ViewPB view, [Map? arguments]) async {
+ Future pushView(
+ ViewPB view, {
+ Map? arguments,
+ bool addInRecent = true,
+ bool showMoreButton = true,
+ String? fixedTitle,
+ }) async {
// set the current view before pushing the new view
getIt().latestOpenView = view;
unawaited(getIt().updateRecentViews([view.id], true));
+ final queryParameters = view.queryParameters(arguments);
+
+ if (view.layout == ViewLayoutPB.Document) {
+ queryParameters[MobileDocumentScreen.viewShowMoreButton] =
+ showMoreButton.toString();
+ if (fixedTitle != null) {
+ queryParameters[MobileDocumentScreen.viewFixedTitle] = fixedTitle;
+ }
+ }
final uri = Uri(
path: view.routeName,
- queryParameters: view.queryParameters(arguments),
+ queryParameters: queryParameters,
).toString();
await push(uri);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart
new file mode 100644
index 0000000000000..193634b2d5825
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/application/notification/notification_reminder_bloc.dart
@@ -0,0 +1,217 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
+import 'package:appflowy/plugins/document/application/document_service.dart';
+import 'package:appflowy/user/application/reminder/reminder_extension.dart';
+import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
+import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
+import 'package:appflowy/workspace/application/view/prelude.dart';
+import 'package:appflowy/workspace/application/view/view_ext.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:appflowy_result/appflowy_result.dart';
+import 'package:bloc/bloc.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:freezed_annotation/freezed_annotation.dart';
+import 'package:time/time.dart';
+
+part 'notification_reminder_bloc.freezed.dart';
+
+class NotificationReminderBloc
+ extends Bloc {
+ NotificationReminderBloc() : super(NotificationReminderState.initial()) {
+ on((event, emit) async {
+ await event.when(
+ initial: (reminder, dateFormat, timeFormat) async {
+ this.reminder = reminder;
+ this.dateFormat = dateFormat;
+ this.timeFormat = timeFormat;
+
+ add(const NotificationReminderEvent.reset());
+ },
+ reset: () async {
+ final createdAt = await _getCreatedAt(
+ reminder,
+ dateFormat,
+ timeFormat,
+ );
+ final view = await _getView(reminder);
+
+ if (view == null) {
+ emit(
+ NotificationReminderState(
+ createdAt: createdAt,
+ pageTitle: '',
+ reminderContent: '',
+ status: NotificationReminderStatus.error,
+ ),
+ );
+ }
+
+ final layout = view!.layout;
+
+ if (layout.isDocumentView) {
+ final node = await _getContent(reminder);
+ if (node != null) {
+ emit(
+ NotificationReminderState(
+ createdAt: createdAt,
+ pageTitle: view.name,
+ view: view,
+ reminderContent: node.delta?.toPlainText() ?? '',
+ nodes: [node],
+ status: NotificationReminderStatus.loaded,
+ ),
+ );
+ }
+ } else if (layout.isDatabaseView) {
+ emit(
+ NotificationReminderState(
+ createdAt: createdAt,
+ pageTitle: view.name,
+ view: view,
+ reminderContent: reminder.message,
+ status: NotificationReminderStatus.loaded,
+ ),
+ );
+ }
+ },
+ );
+ });
+ }
+
+ late final ReminderPB reminder;
+ late final UserDateFormatPB dateFormat;
+ late final UserTimeFormatPB timeFormat;
+
+ Future _getCreatedAt(
+ ReminderPB reminder,
+ UserDateFormatPB dateFormat,
+ UserTimeFormatPB timeFormat,
+ ) async {
+ final rCreatedAt = reminder.createdAt;
+ final createdAt = rCreatedAt != null
+ ? _formatTimestamp(
+ rCreatedAt,
+ timeFormat: timeFormat,
+ dateFormate: dateFormat,
+ )
+ : '';
+ return createdAt;
+ }
+
+ Future _getView(ReminderPB reminder) async {
+ return ViewBackendService.getView(reminder.objectId)
+ .fold((s) => s, (_) => null);
+ }
+
+ Future _getContent(ReminderPB reminder) async {
+ final blockId = reminder.meta[ReminderMetaKeys.blockId];
+
+ if (blockId == null) {
+ return null;
+ }
+
+ final document = await DocumentService()
+ .openDocument(
+ documentId: reminder.objectId,
+ )
+ .fold((s) => s.toDocument(), (_) => null);
+
+ if (document == null) {
+ return null;
+ }
+
+ final node = _searchById(document.root, blockId);
+
+ if (node == null) {
+ return null;
+ }
+
+ return node;
+ }
+
+ Node? _searchById(Node current, String id) {
+ if (current.id == id) {
+ return current;
+ }
+
+ if (current.children.isNotEmpty) {
+ for (final child in current.children) {
+ final node = _searchById(child, id);
+
+ if (node != null) {
+ return node;
+ }
+ }
+ }
+
+ return null;
+ }
+
+ String _formatTimestamp(
+ int timestamp, {
+ required UserDateFormatPB dateFormate,
+ required UserTimeFormatPB timeFormat,
+ }) {
+ final now = DateTime.now();
+ final dateTime = DateTime.fromMillisecondsSinceEpoch(timestamp);
+ final difference = now.difference(dateTime);
+ final String date;
+
+ if (difference.inMinutes < 1) {
+ date = LocaleKeys.sideBar_justNow.tr();
+ } else if (difference.inHours < 1 && dateTime.isToday) {
+ // Less than 1 hour
+ date = LocaleKeys.sideBar_minutesAgo
+ .tr(namedArgs: {'count': difference.inMinutes.toString()});
+ } else if (difference.inHours >= 1 && dateTime.isToday) {
+ // in same day
+ date = timeFormat.formatTime(dateTime);
+ } else {
+ date = dateFormate.formatDate(dateTime, false);
+ }
+
+ return date;
+ }
+}
+
+@freezed
+class NotificationReminderEvent with _$NotificationReminderEvent {
+ const factory NotificationReminderEvent.initial(
+ ReminderPB reminder,
+ UserDateFormatPB dateFormat,
+ UserTimeFormatPB timeFormat,
+ ) = _Initial;
+
+ const factory NotificationReminderEvent.reset() = _Reset;
+}
+
+enum NotificationReminderStatus {
+ initial,
+ loading,
+ loaded,
+ error,
+}
+
+@freezed
+class NotificationReminderState with _$NotificationReminderState {
+ const NotificationReminderState._();
+
+ const factory NotificationReminderState({
+ required String createdAt,
+ required String pageTitle,
+ required String reminderContent,
+ @Default(NotificationReminderStatus.initial)
+ NotificationReminderStatus status,
+ @Default([]) List nodes,
+ ViewPB? view,
+ }) = _NotificationReminderState;
+
+ factory NotificationReminderState.initial() =>
+ const NotificationReminderState(
+ createdAt: '',
+ pageTitle: '',
+ reminderContent: '',
+ );
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart
index 52552fce3b11d..4dde5cc10243c 100644
--- a/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart
+++ b/frontend/appflowy_flutter/lib/mobile/application/page_style/document_page_style_bloc.dart
@@ -23,6 +23,9 @@ class DocumentPageStyleBloc
await event.when(
initial: () async {
try {
+ if (view.id.isEmpty) {
+ return;
+ }
final layoutObject =
await ViewBackendService.getView(view.id).fold(
(s) => jsonDecode(s.extra),
@@ -146,7 +149,7 @@ class DocumentPageStyleBloc
) {
double padding = switch (fontLayout) {
PageStyleFontLayout.small => 1.0,
- PageStyleFontLayout.normal => 2.0,
+ PageStyleFontLayout.normal => 1.0,
PageStyleFontLayout.large => 4.0,
};
switch (lineHeightLayout) {
@@ -162,6 +165,16 @@ class DocumentPageStyleBloc
return max(0, padding);
}
+ double calculateIconScale(
+ PageStyleFontLayout fontLayout,
+ ) {
+ return switch (fontLayout) {
+ PageStyleFontLayout.small => 0.8,
+ PageStyleFontLayout.normal => 1.0,
+ PageStyleFontLayout.large => 1.2,
+ };
+ }
+
PageStyleFontLayout _getSelectedFontLayout(Map layoutObject) {
final fontLayout = layoutObject[ViewExtKeys.fontLayoutKey] ??
PageStyleFontLayout.normal.toString();
diff --git a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart
index 547c81f00b988..99098f930d82e 100644
--- a/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart
+++ b/frontend/appflowy_flutter/lib/mobile/application/recent/recent_view_bloc.dart
@@ -1,7 +1,5 @@
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
-import 'package:appflowy/plugins/document/application/document_data_pb_extension.dart';
import 'package:appflowy/plugins/document/application/document_listener.dart';
-import 'package:appflowy/plugins/document/application/document_service.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/workspace/application/view/prelude.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
@@ -113,7 +111,6 @@ class RecentViewBloc extends Bloc {
);
}
- final _service = DocumentService();
final ViewPB view;
final DocumentListener _documentListener;
final ViewListener _viewListener;
@@ -124,16 +121,6 @@ class RecentViewBloc extends Bloc {
// for the version under 0.5.5
Future<(CoverType, String?)> getCoverV1() async {
- final result = await _service.getDocument(documentId: view.id);
- final document = result.fold((s) => s.toDocument(), (f) => null);
- if (document != null) {
- final coverType = CoverType.fromString(
- document.root.attributes[DocumentHeaderBlockKeys.coverType],
- );
- final coverValue = document
- .root.attributes[DocumentHeaderBlockKeys.coverDetails] as String?;
- return (coverType, coverValue);
- }
return (CoverType.none, null);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart b/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart
index 7edec07cc16ae..1480cc02e9763 100644
--- a/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart
+++ b/frontend/appflowy_flutter/lib/mobile/application/user_profile/user_profile_bloc.dart
@@ -12,12 +12,12 @@ class UserProfileBloc extends Bloc {
UserProfileBloc() : super(const _Initial()) {
on((event, emit) async {
await event.when(
- started: () async => _initalize(emit),
+ started: () async => _initialize(emit),
);
});
}
- Future _initalize(Emitter emit) async {
+ Future _initialize(Emitter emit) async {
emit(const UserProfileState.loading());
final workspaceOrFailure =
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart
new file mode 100644
index 0000000000000..d0e973ae6447a
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/animated_gesture.dart
@@ -0,0 +1,54 @@
+import 'package:appflowy/shared/feedback_gesture_detector.dart';
+import 'package:flutter/material.dart';
+
+class AnimatedGestureDetector extends StatefulWidget {
+ const AnimatedGestureDetector({
+ super.key,
+ this.scaleFactor = 0.98,
+ this.feedback = true,
+ this.duration = const Duration(milliseconds: 100),
+ this.alignment = Alignment.center,
+ this.behavior = HitTestBehavior.opaque,
+ this.onTapUp,
+ required this.child,
+ });
+
+ final Widget child;
+ final double scaleFactor;
+ final Duration duration;
+ final Alignment alignment;
+ final bool feedback;
+ final HitTestBehavior behavior;
+ final VoidCallback? onTapUp;
+
+ @override
+ State createState() =>
+ _AnimatedGestureDetectorState();
+}
+
+class _AnimatedGestureDetectorState extends State {
+ double scale = 1.0;
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ behavior: widget.behavior,
+ onTapUp: (details) {
+ setState(() => scale = 1.0);
+
+ HapticFeedbackType.light.call();
+
+ widget.onTapUp?.call();
+ },
+ onTapDown: (details) {
+ setState(() => scale = widget.scaleFactor);
+ },
+ child: AnimatedScale(
+ scale: scale,
+ alignment: widget.alignment,
+ duration: widget.duration,
+ child: widget.child,
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart
index 335f1af489f9e..396ecd6bb8630 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar.dart
@@ -10,7 +10,7 @@ enum FlowyAppBarLeadingType {
Widget getWidget(VoidCallback? onTap) {
switch (this) {
case FlowyAppBarLeadingType.back:
- return AppBarBackButton(onTap: onTap);
+ return AppBarImmersiveBackButton(onTap: onTap);
case FlowyAppBarLeadingType.close:
return AppBarCloseButton(onTap: onTap);
case FlowyAppBarLeadingType.cancel:
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart
index b59c1e68ccae4..72142d446bf84 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/app_bar/app_bar_actions.dart
@@ -26,6 +26,31 @@ class AppBarBackButton extends StatelessWidget {
}
}
+class AppBarImmersiveBackButton extends StatelessWidget {
+ const AppBarImmersiveBackButton({
+ super.key,
+ this.onTap,
+ });
+
+ final VoidCallback? onTap;
+
+ @override
+ Widget build(BuildContext context) {
+ return AppBarButton(
+ onTap: (_) => (onTap ?? () => Navigator.pop(context)).call(),
+ padding: const EdgeInsets.only(
+ left: 12.0,
+ top: 8.0,
+ bottom: 8.0,
+ right: 4.0,
+ ),
+ child: const FlowySvg(
+ FlowySvgs.m_app_bar_back_s,
+ ),
+ );
+ }
+}
+
class AppBarCloseButton extends StatelessWidget {
const AppBarCloseButton({
super.key,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
index 9e21a002b1c36..569cdd5fe61d5 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/mobile_view_page.dart
@@ -3,8 +3,8 @@ import 'package:appflowy/mobile/application/base/mobile_view_page_bloc.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/view_page/app_bar_buttons.dart';
+import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_state_container.dart';
-import 'package:appflowy/plugins/base/emoji/emoji_text.dart';
import 'package:appflowy/plugins/document/presentation/document_collaborators.dart';
import 'package:appflowy/shared/feature_flags.dart';
import 'package:appflowy/startup/plugin/plugin.dart';
@@ -27,6 +27,8 @@ class MobileViewPage extends StatefulWidget {
required this.viewLayout,
this.title,
this.arguments,
+ this.fixedTitle,
+ this.showMoreButton = true,
});
/// view id
@@ -34,6 +36,10 @@ class MobileViewPage extends StatefulWidget {
final ViewLayoutPB viewLayout;
final String? title;
final Map? arguments;
+ final bool showMoreButton;
+
+ // only used in row page
+ final String? fixedTitle;
@override
State createState() => _MobileViewPageState();
@@ -46,10 +52,18 @@ class _MobileViewPageState extends State {
// control the app bar opacity when in immersive mode
final ValueNotifier _appBarOpacity = ValueNotifier(1.0);
+ @override
+ void initState() {
+ super.initState();
+
+ getIt().add(const ReminderEvent.started());
+ }
+
@override
void dispose() {
_appBarOpacity.dispose();
_scrollNotificationObserver = null;
+
super.dispose();
}
@@ -78,8 +92,7 @@ class _MobileViewPageState extends State {
ViewBloc(view: view)..add(const ViewEvent.initial()),
),
BlocProvider.value(
- value: getIt()
- ..add(const ReminderEvent.started()),
+ value: getIt(),
),
if (view.layout.isDocumentView)
BlocProvider(
@@ -125,7 +138,7 @@ class _MobileViewPageState extends State {
return child;
},
)
- : child;
+ : SafeArea(child: child);
return Scaffold(
extendBodyBehindAppBar: isDocument,
appBar: appBar,
@@ -157,6 +170,9 @@ class _MobileViewPageState extends State {
return plugin.widgetBuilder.buildWidget(
shrinkWrap: false,
context: PluginContext(userProfile: state.userProfilePB),
+ data: {
+ MobileDocumentScreen.viewFixedTitle: widget.fixedTitle,
+ },
);
},
(error) {
@@ -209,13 +225,19 @@ class _MobileViewPageState extends State {
]);
}
- actions.addAll([
- MobileViewPageMoreButton(
- view: view,
- isImmersiveMode: isImmersiveMode,
- appBarOpacity: _appBarOpacity,
- ),
- ]);
+ if (widget.showMoreButton) {
+ actions.addAll([
+ MobileViewPageMoreButton(
+ view: view,
+ isImmersiveMode: isImmersiveMode,
+ appBarOpacity: _appBarOpacity,
+ ),
+ ]);
+ } else {
+ actions.addAll([
+ const HSpace(18.0),
+ ]);
+ }
return actions;
}
@@ -225,19 +247,20 @@ class _MobileViewPageState extends State {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
- if (icon != null && icon.isNotEmpty)
- ConstrainedBox(
- constraints: const BoxConstraints.tightFor(width: 34.0),
- child: EmojiText(
- emoji: '$icon ',
- fontSize: 22.0,
- ),
+ if (icon != null && icon.isNotEmpty) ...[
+ FlowyText.emoji(
+ icon,
+ fontSize: 15.0,
+ figmaLineHeight: 18.0,
),
+ const HSpace(4),
+ ],
Expanded(
child: FlowyText.medium(
- view?.name ?? widget.title ?? '',
+ widget.fixedTitle ?? view?.name ?? widget.title ?? '',
fontSize: 15.0,
overflow: TextOverflow.ellipsis,
+ figmaLineHeight: 18.0,
),
),
],
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart
index 497f76935444d..5e4595a1e5d53 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/base/type_option_menu_item.dart
@@ -102,6 +102,7 @@ class _TypeOptionMenuItem extends StatelessWidget {
value.text,
fontSize: 14.0,
maxLines: 2,
+ lineHeight: 1.0,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart
index 1a8ff64f2b639..3316b7049beb8 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_add_new_page.dart
@@ -24,9 +24,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_document_s,
- size: Size.square(18),
+ size: Size.square(20),
),
showTopBorder: false,
+ showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Document),
),
FlowyOptionTile.text(
@@ -34,9 +35,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_grid_s,
- size: Size.square(18),
+ size: Size.square(20),
),
showTopBorder: false,
+ showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Grid),
),
FlowyOptionTile.text(
@@ -44,9 +46,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_board_s,
- size: Size.square(18),
+ size: Size.square(20),
),
showTopBorder: false,
+ showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Board),
),
FlowyOptionTile.text(
@@ -54,9 +57,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.icon_calendar_s,
- size: Size.square(18),
+ size: Size.square(20),
),
showTopBorder: false,
+ showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Calendar),
),
FlowyOptionTile.text(
@@ -64,9 +68,10 @@ class AddNewPageWidgetBottomSheet extends StatelessWidget {
height: 52.0,
leftIcon: const FlowySvg(
FlowySvgs.chat_ai_page_s,
- size: Size.square(18),
+ size: Size.square(20),
),
showTopBorder: false,
+ showBottomBorder: false,
onTap: () => onAction(ViewLayoutPB.Chat),
),
],
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart
index d4f49cb9a97a5..8ac4d9b20e765 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_rename_widget.dart
@@ -52,6 +52,7 @@ class _MobileBottomSheetRenameWidgetState
height: 42.0,
child: FlowyTextField(
controller: controller,
+ textStyle: Theme.of(context).textTheme.bodyMedium,
keyboardType: TextInputType.text,
onSubmitted: (text) => widget.onRename(text),
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart
index 75b0151a3a518..c1129af79dc22 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/bottom_sheet_view_item.dart
@@ -1,4 +1,3 @@
-import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/mobile/presentation/widgets/show_flowy_mobile_confirm_dialog.dart';
@@ -6,6 +5,7 @@ import 'package:appflowy/startup/tasks/app_widget.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -64,6 +64,10 @@ class _MobileViewItemBottomSheetState extends State {
case MobileViewItemBottomSheetBodyAction.duplicate:
Navigator.pop(context);
context.read().add(const ViewEvent.duplicate());
+ showToastNotification(
+ context,
+ message: LocaleKeys.button_duplicateSuccessfully.tr(),
+ );
break;
case MobileViewItemBottomSheetBodyAction.share:
// unimplemented
@@ -79,6 +83,12 @@ class _MobileViewItemBottomSheetState extends State {
context
.read()
.add(FavoriteEvent.toggle(widget.view));
+ showToastNotification(
+ context,
+ message: !widget.view.isFavorite
+ ? LocaleKeys.button_favoriteSuccessfully.tr()
+ : LocaleKeys.button_unfavoriteSuccessfully.tr(),
+ );
break;
case MobileViewItemBottomSheetBodyAction.removeFromRecent:
_removeFromRecent(context);
@@ -109,16 +119,6 @@ class _MobileViewItemBottomSheetState extends State {
await _showConfirmDialog(
onDelete: () {
recentViewsBloc.add(RecentViewsEvent.removeRecentViews([viewId]));
-
- fToast.showToast(
- child: const _RemoveToast(),
- positionedToastBuilder: (context, child) {
- return Positioned.fill(
- top: 450,
- child: child,
- );
- },
- );
},
);
}
@@ -126,48 +126,30 @@ class _MobileViewItemBottomSheetState extends State {
Future _showConfirmDialog({required VoidCallback onDelete}) async {
await showFlowyCupertinoConfirmDialog(
title: LocaleKeys.sideBar_removePageFromRecent.tr(),
- leftButton: FlowyText.regular(
+ leftButton: FlowyText(
LocaleKeys.button_cancel.tr(),
- color: const Color(0xFF1456F0),
+ fontSize: 17.0,
+ figmaLineHeight: 24.0,
+ fontWeight: FontWeight.w500,
+ color: const Color(0xFF007AFF),
),
- rightButton: FlowyText.medium(
+ rightButton: FlowyText(
LocaleKeys.button_delete.tr(),
+ fontSize: 17.0,
+ figmaLineHeight: 24.0,
+ fontWeight: FontWeight.w400,
color: const Color(0xFFFE0220),
),
onRightButtonPressed: (context) {
onDelete();
- Navigator.pop(context);
- },
- );
- }
-}
-class _RemoveToast extends StatelessWidget {
- const _RemoveToast();
+ Navigator.pop(context);
- @override
- Widget build(BuildContext context) {
- return Container(
- padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 13.0),
- decoration: BoxDecoration(
- borderRadius: BorderRadius.circular(12.0),
- color: const Color(0xE5171717),
- ),
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- const FlowySvg(
- FlowySvgs.success_s,
- blendMode: null,
- ),
- const HSpace(8.0),
- FlowyText.regular(
- LocaleKeys.sideBar_removeSuccess.tr(),
- fontSize: 16.0,
- color: Colors.white,
- ),
- ],
- ),
+ showToastNotification(
+ context,
+ message: LocaleKeys.sideBar_removeSuccess.tr(),
+ );
+ },
);
}
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart
index 1583e9e2e015f..e0b10d153b37e 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/default_mobile_action_pane.dart
@@ -7,6 +7,7 @@ import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
import 'package:appflowy/workspace/application/recent/recent_views_bloc.dart';
import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -40,18 +41,32 @@ enum MobilePaneActionType {
backgroundColor: const Color(0xFFFA217F),
svg: FlowySvgs.favorite_section_remove_from_favorite_s,
size: 24.0,
- onPressed: (context) => context
- .read()
- .add(FavoriteEvent.toggle(context.read().view)),
+ onPressed: (context) {
+ showToastNotification(
+ context,
+ message: LocaleKeys.button_unfavoriteSuccessfully.tr(),
+ );
+
+ context
+ .read()
+ .add(FavoriteEvent.toggle(context.read().view));
+ },
);
case MobilePaneActionType.addToFavorites:
return MobileSlideActionButton(
backgroundColor: const Color(0xFF00C8FF),
svg: FlowySvgs.favorite_s,
size: 24.0,
- onPressed: (context) => context
- .read()
- .add(FavoriteEvent.toggle(context.read().view)),
+ onPressed: (context) {
+ showToastNotification(
+ context,
+ message: LocaleKeys.button_favoriteSuccessfully.tr(),
+ );
+
+ context
+ .read()
+ .add(FavoriteEvent.toggle(context.read().view));
+ },
);
case MobilePaneActionType.add:
return MobileSlideActionButton(
@@ -69,6 +84,7 @@ enum MobilePaneActionType {
showDragHandle: true,
showCloseButton: true,
useRootNavigator: true,
+ showDivider: false,
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (sheetContext) {
return AddNewPageWidgetBottomSheet(
@@ -145,8 +161,6 @@ enum MobilePaneActionType {
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
- if (view.layout != ViewLayoutPB.Chat)
- MobileViewItemBottomSheetBodyAction.duplicate,
MobileViewItemBottomSheetBodyAction.divider,
MobileViewItemBottomSheetBodyAction.removeFromRecent,
];
@@ -156,7 +170,6 @@ enum MobilePaneActionType {
? MobileViewItemBottomSheetBodyAction.removeFromFavorites
: MobileViewItemBottomSheetBodyAction.addToFavorites,
MobileViewItemBottomSheetBodyAction.divider,
- MobileViewItemBottomSheetBodyAction.duplicate,
];
}
}
@@ -181,12 +194,13 @@ ActionPane buildEndActionPane(
bool needSpace = true,
MobilePageCardType? cardType,
FolderSpaceType? spaceType,
+ required double spaceRatio,
}) {
return ActionPane(
motion: const ScrollMotion(),
- extentRatio: actions.length / 5,
+ extentRatio: actions.length / spaceRatio,
children: [
- if (needSpace) const HSpace(20),
+ if (needSpace) const HSpace(60),
...actions.map(
(action) => action.actionButton(
context,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
index be815b6550c6a..9af49e98c883f 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/bottom_sheet/show_mobile_bottom_sheet.dart
@@ -70,6 +70,7 @@ Future showMobileBottomSheet(
backgroundColor ??= Theme.of(context).brightness == Brightness.light
? const Color(0xFFF7F8FB)
: const Color(0xFF23262B);
+ barrierColor ??= Colors.black.withOpacity(0.3);
return showModalBottomSheet(
context: context,
@@ -226,10 +227,14 @@ class BottomSheetHeader extends StatelessWidget {
),
),
Align(
- child: FlowyText(
- title,
- fontSize: 16.0,
- fontWeight: FontWeight.w500,
+ child: Container(
+ constraints: const BoxConstraints(maxWidth: 250),
+ child: FlowyText(
+ title,
+ fontSize: 17.0,
+ fontWeight: FontWeight.w500,
+ overflow: TextOverflow.ellipsis,
+ ),
),
),
if (showDoneButton)
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart
index a1fc2a70a3697..90ebd4f61f97e 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/mobile_board_page.dart
@@ -3,13 +3,13 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/board/board.dart';
import 'package:appflowy/mobile/presentation/database/board/widgets/group_card_header.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
-import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
import 'package:appflowy/plugins/database/grid/presentation/widgets/header/field_type_extension.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
+import 'package:appflowy/shared/flowy_error_page.dart';
import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/protobuf.dart';
@@ -69,10 +69,10 @@ class _MobileBoardPageState extends State {
loading: (_) => const Center(
child: CircularProgressIndicator.adaptive(),
),
- error: (err) => FlowyMobileStateContainer.error(
- emoji: '🛸',
- title: LocaleKeys.board_mobile_failedToLoad.tr(),
- errorMsg: err.toString(),
+ error: (err) => Center(
+ child: AppFlowyErrorPage(
+ error: err.error,
+ ),
),
ready: (data) => const _BoardContent(),
orElse: () => const SizedBox.shrink(),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart
index 0b0c16f951e36..f80525786ed6d 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/board/widgets/mobile_hidden_groups_column.dart
@@ -1,9 +1,12 @@
+import 'package:flutter/material.dart';
+
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
import 'package:appflowy/plugins/database/board/application/board_bloc.dart';
+import 'package:appflowy/plugins/database/board/group_ext.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_skeleton/text_card_cell.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
@@ -11,7 +14,6 @@ import 'package:collection/collection.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
@@ -200,7 +202,7 @@ class MobileHiddenGroup extends StatelessWidget {
children: [
Expanded(
child: Text(
- context.read().generateGroupNameFromGroup(group),
+ group.generateGroupName(databaseController),
style: Theme.of(context).textTheme.bodyMedium,
maxLines: 2,
overflow: TextOverflow.ellipsis,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart
index c65f899c34c74..4b080e540dda7 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/mobile_card_detail_screen.dart
@@ -1,8 +1,11 @@
+import 'package:flutter/material.dart';
+
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar_actions.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
+import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart';
import 'package:appflowy/mobile/presentation/widgets/flowy_mobile_quick_action_button.dart';
import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
@@ -22,7 +25,6 @@ import 'package:appflowy_backend/protobuf/flowy-database2/row_entities.pb.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra/theme_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:go_router/go_router.dart';
@@ -294,6 +296,7 @@ class MobileRowDetailPageContentState
RowCache get rowCache => widget.databaseController.rowCache;
FieldController get fieldController =>
widget.databaseController.fieldController;
+ ValueNotifier primaryFieldId = ValueNotifier('');
@override
void initState() {
@@ -304,6 +307,8 @@ class MobileRowDetailPageContentState
viewId: viewId,
rowCache: rowCache,
);
+ rowController.initialize();
+
cellBuilder = EditableCellBuilder(
databaseController: widget.databaseController,
);
@@ -326,7 +331,13 @@ class MobileRowDetailPageContentState
fieldController: fieldController,
rowMeta: rowController.rowMeta,
)..add(const RowBannerEvent.initial()),
- child: BlocBuilder(
+ child: BlocConsumer(
+ listener: (context, state) {
+ if (state.primaryField == null) {
+ return;
+ }
+ primaryFieldId.value = state.primaryField!.id;
+ },
builder: (context, state) {
if (state.primaryField == null) {
return const SizedBox.shrink();
@@ -366,6 +377,23 @@ class MobileRowDetailPageContentState
if (rowDetailState.numHiddenFields != 0) ...[
const ToggleHiddenFieldsVisibilityButton(),
],
+ const VSpace(8.0),
+ ValueListenableBuilder(
+ valueListenable: primaryFieldId,
+ builder: (context, primaryFieldId, child) {
+ if (primaryFieldId.isEmpty) {
+ return const SizedBox.shrink();
+ }
+ return OpenRowPageButton(
+ databaseController: widget.databaseController,
+ cellContext: CellContext(
+ rowId: rowController.rowId,
+ fieldId: primaryFieldId,
+ ),
+ documentId: rowController.rowMeta.documentId,
+ );
+ },
+ ),
MobileRowDetailCreateFieldButton(
viewId: viewId,
fieldController: fieldController,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart
index d683a9b72dfc3..1d3d3efcf5579 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_create_field_button.dart
@@ -22,7 +22,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
return ConstrainedBox(
constraints: BoxConstraints(
minWidth: double.infinity,
- minHeight: GridSize.headerHeight,
+ maxHeight: GridSize.headerHeight,
),
child: TextButton.icon(
style: Theme.of(context).textButtonTheme.style?.copyWith(
@@ -37,7 +37,7 @@ class MobileRowDetailCreateFieldButton extends StatelessWidget {
alignment: AlignmentDirectional.centerStart,
splashFactory: NoSplash.splashFactory,
padding: const WidgetStatePropertyAll(
- EdgeInsets.symmetric(vertical: 14, horizontal: 6),
+ EdgeInsets.symmetric(horizontal: 6, vertical: 2),
),
),
label: FlowyText.medium(
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart
index 04984275474f1..95298daeff3ae 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/mobile_row_property_list.dart
@@ -1,3 +1,5 @@
+import 'package:flutter/material.dart';
+
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/plugins/database/application/cell/cell_controller.dart';
import 'package:appflowy/plugins/database/application/database_controller.dart';
@@ -6,7 +8,6 @@ import 'package:appflowy/plugins/database/grid/application/row/row_detail_bloc.d
import 'package:appflowy/plugins/database/widgets/cell/editable_cell_builder.dart';
import 'package:appflowy/util/field_type_extension.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
-import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
class MobileRowPropertyList extends StatelessWidget {
@@ -87,6 +88,7 @@ class _PropertyCellState extends State<_PropertyCell> {
fieldInfo.name,
overflow: TextOverflow.ellipsis,
fontSize: 14,
+ figmaLineHeight: 16.0,
color: Theme.of(context).hintColor,
),
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart
new file mode 100644
index 0000000000000..49f95887ab8fc
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/card_detail/widgets/row_page_button.dart
@@ -0,0 +1,143 @@
+import 'dart:async';
+
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/application/mobile_router.dart';
+import 'package:appflowy/plugins/database/application/cell/bloc/text_cell_bloc.dart';
+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/workspace/application/view/prelude.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package: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';
+
+class OpenRowPageButton extends StatefulWidget {
+ const OpenRowPageButton({
+ super.key,
+ required this.documentId,
+ required this.databaseController,
+ required this.cellContext,
+ });
+
+ final String documentId;
+
+ final DatabaseController databaseController;
+ final CellContext cellContext;
+
+ @override
+ State createState() => _OpenRowPageButtonState();
+}
+
+class _OpenRowPageButtonState extends State {
+ late final cellBloc = TextCellBloc(
+ cellController: makeCellController(
+ widget.databaseController,
+ widget.cellContext,
+ ).as(),
+ );
+
+ ViewPB? view;
+
+ @override
+ void initState() {
+ super.initState();
+
+ _preloadView(context, createDocumentIfMissed: true);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ bloc: cellBloc,
+ builder: (context, state) {
+ return ConstrainedBox(
+ constraints: BoxConstraints(
+ minWidth: double.infinity,
+ maxHeight: GridSize.buttonHeight,
+ ),
+ child: TextButton.icon(
+ style: Theme.of(context).textButtonTheme.style?.copyWith(
+ shape: WidgetStateProperty.all(
+ RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12.0),
+ ),
+ ),
+ overlayColor: WidgetStateProperty.all(
+ Theme.of(context).hoverColor,
+ ),
+ alignment: AlignmentDirectional.centerStart,
+ splashFactory: NoSplash.splashFactory,
+ padding: const WidgetStatePropertyAll(
+ EdgeInsets.symmetric(horizontal: 6),
+ ),
+ ),
+ label: FlowyText.medium(
+ LocaleKeys.grid_field_openRowDocument.tr(),
+ fontSize: 15,
+ ),
+ icon: const Padding(
+ padding: EdgeInsets.all(4.0),
+ child: FlowySvg(
+ FlowySvgs.full_view_s,
+ size: Size.square(16.0),
+ ),
+ ),
+ onPressed: () {
+ final name = state.content;
+ _openRowPage(context, name);
+ },
+ ),
+ );
+ },
+ );
+ }
+
+ Future _openRowPage(BuildContext context, String fieldName) async {
+ Log.info('Open row page(${widget.documentId})');
+
+ if (view == null) {
+ showToastNotification(context, message: 'Failed to open row page');
+ // reload the view again
+ unawaited(_preloadView(context));
+ Log.error('Failed to open row page(${widget.documentId})');
+ return;
+ }
+
+ if (context.mounted) {
+ // the document in row is an orphan document, so we don't add it to recent
+ await context.pushView(
+ view!,
+ addInRecent: false,
+ showMoreButton: false,
+ fixedTitle: fieldName,
+ );
+ }
+ }
+
+ // preload view to reduce the time to open the view
+ Future _preloadView(
+ BuildContext context, {
+ bool createDocumentIfMissed = false,
+ }) async {
+ Log.info('Preload row page(${widget.documentId})');
+ final result = await ViewBackendService.getView(widget.documentId);
+ view = result.fold((s) => s, (f) => null);
+
+ if (view == null && createDocumentIfMissed) {
+ // create view if not exists
+ Log.info('Create row page(${widget.documentId})');
+ final result = await ViewBackendService.createOrphanView(
+ name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ viewId: widget.documentId,
+ layoutType: ViewLayoutPB.Document,
+ );
+ view = result.fold((s) => s, (f) => null);
+ }
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart
index 48d8b2f097374..26978410051ef 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/card/mobile_card_content.dart
@@ -1,9 +1,16 @@
+import 'package:flutter/material.dart';
+
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/plugins/database/widgets/card/card.dart';
import 'package:appflowy/plugins/database/widgets/card/card_bloc.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_builder.dart';
import 'package:appflowy/plugins/database/widgets/cell/card_cell_style_maps/mobile_board_card_cell_style.dart';
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.dart';
-import 'package:flutter/material.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra/theme_extension.dart';
+import 'package:flowy_infra_ui/style_widget/text.dart';
+import 'package:flowy_infra_ui/widget/spacing.dart';
class MobileCardContent extends StatelessWidget {
const MobileCardContent({
@@ -21,19 +28,47 @@ class MobileCardContent extends StatelessWidget {
@override
Widget build(BuildContext context) {
+ final attachmentCount = rowMeta.attachmentCount.toInt();
+
return Padding(
padding: styleConfiguration.cardPadding,
child: Column(
mainAxisSize: MainAxisSize.min,
- children: cells.map(
- (cellMeta) {
- return cellBuilder.build(
- cellContext: cellMeta.cellContext(),
- styleMap: mobileBoardCardCellStyleMap(context),
- hasNotes: !rowMeta.isDocumentEmpty,
- );
- },
- ).toList(),
+ children: [
+ ...cells.map(
+ (cellMeta) {
+ return cellBuilder.build(
+ cellContext: cellMeta.cellContext(),
+ styleMap: mobileBoardCardCellStyleMap(context),
+ hasNotes: !rowMeta.isDocumentEmpty,
+ );
+ },
+ ),
+ if (attachmentCount > 0) ...[
+ const VSpace(4),
+ Padding(
+ padding: const EdgeInsets.only(left: 8),
+ child: Row(
+ children: [
+ const FlowySvg(
+ FlowySvgs.media_s,
+ size: Size.square(12),
+ ),
+ const HSpace(6),
+ Flexible(
+ child: FlowyText.regular(
+ LocaleKeys.grid_media_attachmentsHint
+ .tr(args: ['$attachmentCount']),
+ fontSize: 12,
+ color: AFThemeExtension.of(context).secondaryTextColor,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ],
+ ],
),
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart
index d794817339bf4..a51e3561f8464 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_edit_field_screen.dart
@@ -1,12 +1,13 @@
+import 'package:flutter/material.dart';
+
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/database/field/mobile_full_field_editor.dart';
-import 'package:appflowy/plugins/database/domain/field_backend_service.dart';
import 'package:appflowy/plugins/database/application/field/field_info.dart';
+import 'package:appflowy/plugins/database/domain/field_backend_service.dart';
import 'package:appflowy/plugins/database/domain/field_service.dart';
import 'package:appflowy/plugins/database/widgets/setting/field_visibility_extension.dart';
import 'package:easy_localization/easy_localization.dart';
-import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
class MobileEditPropertyScreen extends StatefulWidget {
@@ -49,7 +50,7 @@ class _MobileEditPropertyScreenState extends State {
return PopScope(
onPopInvoked: (didPop) {
- if (didPop) {
+ if (!didPop) {
context.pop(_fieldOptionValues);
}
},
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart
index fb1bda724d310..74c7a5a56b141 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_field_bottom_sheets.dart
@@ -29,6 +29,7 @@ const mobileSupportedFieldTypes = [
FieldType.CreatedTime,
FieldType.Checkbox,
FieldType.Checklist,
+ FieldType.Media,
// FieldType.Time,
];
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart
index 0b415b04a67bd..311794aa42ec0 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/field/mobile_quick_field_editor.dart
@@ -1,3 +1,5 @@
+import 'package:flutter/material.dart';
+
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/database/card/card_detail/widgets/widgets.dart';
@@ -11,7 +13,6 @@ import 'package:appflowy/plugins/database/widgets/setting/field_visibility_exten
import 'package:appflowy_backend/protobuf/flowy-database2/protobuf.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';
@@ -61,7 +62,8 @@ class _QuickEditFieldState extends State {
create: (_) => FieldEditorBloc(
viewId: widget.viewId,
fieldController: widget.fieldController,
- field: widget.fieldInfo.field,
+ fieldInfo: widget.fieldInfo,
+ isNew: false,
),
child: BlocConsumer(
listenWhen: (previous, current) =>
@@ -99,7 +101,7 @@ class _QuickEditFieldState extends State {
context.pop();
},
),
- if (!widget.fieldInfo.isPrimary)
+ if (!widget.fieldInfo.isPrimary) ...[
FlowyOptionTile.text(
showTopBorder: false,
text: fieldVisibility.isVisibleState()
@@ -115,7 +117,6 @@ class _QuickEditFieldState extends State {
}
},
),
- if (!widget.fieldInfo.isPrimary)
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertLeft.tr(),
@@ -132,6 +133,7 @@ class _QuickEditFieldState extends State {
);
},
),
+ ],
FlowyOptionTile.text(
showTopBorder: false,
text: LocaleKeys.grid_field_insertRight.tr(),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart
index 4d8acbbeba024..f5812541c8336 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/database/view/edit_database_view_screen.dart
@@ -133,7 +133,7 @@ enum DatabaseViewSettings {
filter => FlowySvgs.filter_s,
sort => FlowySvgs.sort_ascending_s,
board => FlowySvgs.board_s,
- calendar => FlowySvgs.date_s,
+ calendar => FlowySvgs.calendar_s,
duplicate => FlowySvgs.copy_s,
delete => FlowySvgs.delete_s,
};
@@ -176,6 +176,7 @@ class DatabaseViewSettingTile extends StatelessWidget {
return Row(
children: [
FlowyText(
+ lineHeight: 1.0,
databaseLayoutFromViewLayout(view.layout).layoutName,
color: Theme.of(context).hintColor,
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart
index 14c4e022aef45..aacc055e7474f 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/editor/mobile_editor_screen.dart
@@ -7,15 +7,21 @@ class MobileDocumentScreen extends StatelessWidget {
super.key,
required this.id,
this.title,
+ this.showMoreButton = true,
+ this.fixedTitle,
});
/// view id
final String id;
final String? title;
+ final bool showMoreButton;
+ final String? fixedTitle;
static const routeName = '/docs';
static const viewId = 'id';
static const viewTitle = 'title';
+ static const viewShowMoreButton = 'show_more_button';
+ static const viewFixedTitle = 'fixed_title';
@override
Widget build(BuildContext context) {
@@ -23,6 +29,8 @@ class MobileDocumentScreen extends StatelessWidget {
id: id,
title: title,
viewLayout: ViewLayoutPB.Document,
+ showMoreButton: showMoreButton,
+ fixedTitle: fixedTitle,
);
}
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart
index ded486983ef0e..6282421109071 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/favorite_space.dart
@@ -96,38 +96,34 @@ class _FavoriteViews extends StatelessWidget {
final borderColor = Theme.of(context).isLightMode
? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF);
- return Scrollbar(
- child: ListView.separated(
- key: const PageStorageKey('favorite_views_page_storage_key'),
- padding: EdgeInsets.only(
- left: HomeSpaceViewSizes.mHorizontalPadding,
- right: HomeSpaceViewSizes.mHorizontalPadding,
- bottom: HomeSpaceViewSizes.mVerticalPadding +
- MediaQuery.of(context).padding.bottom,
- ),
- itemBuilder: (context, index) {
- final view = favoriteViews[index];
- return Container(
- padding: const EdgeInsets.symmetric(vertical: 24.0),
- decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: borderColor,
- width: 0.5,
- ),
+ return ListView.separated(
+ key: const PageStorageKey('favorite_views_page_storage_key'),
+ padding: EdgeInsets.only(
+ bottom: HomeSpaceViewSizes.mVerticalPadding +
+ MediaQuery.of(context).padding.bottom,
+ ),
+ itemBuilder: (context, index) {
+ final view = favoriteViews[index];
+ return Container(
+ padding: const EdgeInsets.symmetric(vertical: 24.0),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: borderColor,
+ width: 0.5,
),
),
- child: MobileViewPage(
- key: ValueKey(view.item.id),
- view: view.item,
- timestamp: view.timestamp,
- type: MobilePageCardType.favorite,
- ),
- );
- },
- separatorBuilder: (context, index) => const HSpace(8),
- itemCount: favoriteViews.length,
- ),
+ ),
+ child: MobileViewPage(
+ key: ValueKey(view.item.id),
+ view: view.item,
+ timestamp: view.timestamp,
+ type: MobilePageCardType.favorite,
+ ),
+ );
+ },
+ separatorBuilder: (context, index) => const HSpace(8),
+ itemCount: favoriteViews.length,
);
}
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart
index 57ac43f255c1b..1efee460ebacd 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/favorite_folder/mobile_home_favorite_folder.dart
@@ -72,6 +72,7 @@ class MobileFavoriteFolder extends StatelessWidget {
MobilePaneActionType.more,
],
spaceType: FolderSpaceType.favorite,
+ spaceRatio: 5,
),
),
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart
index 965c396d42655..56513795226ab 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/home_space/home_space.dart
@@ -25,21 +25,17 @@ class _MobileHomeSpaceState extends State
final workspaceId =
context.read().state.currentWorkspace?.workspaceId ??
'';
- return Scrollbar(
- child: SingleChildScrollView(
- child: Padding(
- padding: EdgeInsets.only(
- left: HomeSpaceViewSizes.mHorizontalPadding,
- right: HomeSpaceViewSizes.mHorizontalPadding,
- top: HomeSpaceViewSizes.mVerticalPadding,
- bottom: HomeSpaceViewSizes.mVerticalPadding +
- MediaQuery.of(context).padding.bottom,
- ),
- child: MobileFolders(
- user: widget.userProfile,
- workspaceId: workspaceId,
- showFavorite: false,
- ),
+ return SingleChildScrollView(
+ child: Padding(
+ padding: EdgeInsets.only(
+ top: HomeSpaceViewSizes.mVerticalPadding,
+ bottom: HomeSpaceViewSizes.mVerticalPadding +
+ MediaQuery.of(context).padding.bottom,
+ ),
+ child: MobileFolders(
+ user: widget.userProfile,
+ workspaceId: workspaceId,
+ showFavorite: false,
),
),
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
index 64ad7e4cd1a4e..0013650df94ac 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_folders.dart
@@ -1,10 +1,6 @@
-import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
-import 'package:appflowy/mobile/application/mobile_router.dart';
-import 'package:appflowy/mobile/presentation/home/home.dart';
import 'package:appflowy/mobile/presentation/home/section_folder/mobile_home_section_folder.dart';
import 'package:appflowy/mobile/presentation/home/space/mobile_space.dart';
-import 'package:appflowy/workspace/application/favorite/favorite_bloc.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';
@@ -15,7 +11,6 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
-import 'package:go_router/go_router.dart';
// Contains Public And Private Sections
class MobileFolders extends StatelessWidget {
@@ -32,73 +27,54 @@ class MobileFolders extends StatelessWidget {
@override
Widget build(BuildContext context) {
- return MultiBlocProvider(
- providers: [
- BlocProvider(
- create: (_) => SidebarSectionsBloc()
- ..add(SidebarSectionsEvent.initial(user, workspaceId)),
- ),
- BlocProvider(
- create: (_) => FavoriteBloc()..add(const FavoriteEvent.initial()),
- ),
- BlocProvider(
- create: (_) => SpaceBloc()
- ..add(SpaceEvent.initial(user, workspaceId, openFirstPage: false)),
- ),
- ],
- child: BlocListener(
- listener: (context, state) {
- context.read().add(
- SidebarSectionsEvent.initial(
- user,
- state.currentWorkspace?.workspaceId ?? workspaceId,
- ),
- );
- context.read().add(
- SpaceEvent.reset(
- user,
- state.currentWorkspace?.workspaceId ?? workspaceId,
- ),
- );
- },
- child: MultiBlocListener(
- listeners: [
- BlocListener(
- listenWhen: (p, c) =>
- p.lastCreatedPage?.id != c.lastCreatedPage?.id,
- listener: (context, state) {
- final lastCreatedPage = state.lastCreatedPage;
- if (lastCreatedPage != null) {
- context.pushView(lastCreatedPage);
- }
- },
- ),
- BlocListener(
- listenWhen: (p, c) =>
- p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
- listener: (context, state) {
- final lastCreatedPage = state.lastCreatedRootView;
- if (lastCreatedPage != null) {
- context.pushView(lastCreatedPage);
- }
- },
- ),
- ],
- child: BlocBuilder(
- builder: (context, state) {
- return SlidableAutoCloseBehavior(
- child: Column(
- children: [
- ..._buildSpaceOrSection(context, state),
- const VSpace(4.0),
- const _TrashButton(),
- ],
- ),
- );
- },
+ final workspaceId =
+ context.read().state.currentWorkspace?.workspaceId ??
+ '';
+ return BlocListener(
+ listenWhen: (previous, current) =>
+ previous.currentWorkspace?.workspaceId !=
+ current.currentWorkspace?.workspaceId,
+ listener: (context, state) {
+ context.read().add(
+ SidebarSectionsEvent.initial(
+ user,
+ state.currentWorkspace?.workspaceId ?? workspaceId,
+ ),
+ );
+ context.read().add(
+ SpaceEvent.reset(
+ user,
+ state.currentWorkspace?.workspaceId ?? workspaceId,
+ false,
+ ),
+ );
+ },
+ child: const _MobileFolder(),
+ );
+ }
+}
+
+class _MobileFolder extends StatefulWidget {
+ const _MobileFolder();
+
+ @override
+ State<_MobileFolder> createState() => _MobileFolderState();
+}
+
+class _MobileFolderState extends State<_MobileFolder> {
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (context, state) {
+ return SlidableAutoCloseBehavior(
+ child: Column(
+ children: [
+ ..._buildSpaceOrSection(context, state),
+ const VSpace(80.0),
+ ],
),
- ),
- ),
+ );
+ },
);
}
@@ -137,28 +113,3 @@ class MobileFolders extends StatelessWidget {
];
}
}
-
-class _TrashButton extends StatelessWidget {
- const _TrashButton();
-
- @override
- Widget build(BuildContext context) {
- return SizedBox(
- height: 52,
- child: FlowyButton(
- expand: true,
- margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 2.0),
- leftIcon: const FlowySvg(
- FlowySvgs.m_delete_s,
- ),
- leftIconSize: const Size.square(18),
- iconPadding: 10.0,
- text: FlowyText.regular(
- LocaleKeys.trash_text.tr(),
- fontSize: 16.0,
- ),
- onTap: () => context.push(MobileHomeTrashPage.routeName),
- ),
- );
- }
-}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
index 1dc72f370d3a2..f2d866533d0ee 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page.dart
@@ -1,25 +1,32 @@
-import 'dart:io';
-
-import 'package:flutter/material.dart';
-
+import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/presentation/home/mobile_home_page_header.dart';
import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
import 'package:appflowy/mobile/presentation/home/tab/space_order_bloc.dart';
+import 'package:appflowy/plugins/document/presentation/editor_plugins/openai/widgets/loading.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/workspace/application/favorite/favorite_bloc.dart';
+import 'package:appflowy/workspace/application/menu/sidebar_sections_bloc.dart';
import 'package:appflowy/workspace/application/recent/cached_recent_service.dart';
+import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
import 'package:appflowy/workspace/presentation/home/errors/workspace_failed_screen.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/menu_shared_state.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/dispatch/dispatch.dart';
+import 'package:appflowy_backend/log.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/workspace.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:appflowy_editor/appflowy_editor.dart' hide Log;
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
+import 'package:sentry/sentry.dart';
+import 'package:toastification/toastification.dart';
class MobileHomeScreen extends StatelessWidget {
const MobileHomeScreen({super.key});
@@ -57,6 +64,14 @@ class MobileHomeScreen extends StatelessWidget {
return const WorkspaceFailedScreen();
}
+ Sentry.configureScope(
+ (scope) => scope.setUser(
+ SentryUser(
+ id: userProfile.id.toString(),
+ ),
+ ),
+ );
+
return Scaffold(
body: SafeArea(
bottom: false,
@@ -74,6 +89,9 @@ class MobileHomeScreen extends StatelessWidget {
}
}
+final PropertyValueNotifier mCurrentWorkspace =
+ PropertyValueNotifier(null);
+
class MobileHomePage extends StatefulWidget {
const MobileHomePage({
super.key,
@@ -89,16 +107,20 @@ class MobileHomePage extends StatefulWidget {
}
class _MobileHomePageState extends State {
+ Loading? loadingIndicator;
+
@override
void initState() {
super.initState();
getIt().addLatestViewListener(_onLatestViewChange);
+ getIt().add(const ReminderEvent.started());
}
@override
void dispose() {
getIt().removeLatestViewListener(_onLatestViewChange);
+
super.dispose();
}
@@ -118,51 +140,167 @@ class _MobileHomePageState extends State {
value: getIt()..add(const ReminderEvent.started()),
),
],
- child: BlocConsumer(
- buildWhen: (previous, current) =>
- previous.currentWorkspace?.workspaceId !=
- current.currentWorkspace?.workspaceId,
- listener: (context, state) => getIt().reset(),
- builder: (context, state) {
- if (state.currentWorkspace == null) {
- return const SizedBox.shrink();
- }
-
- return Column(
- children: [
- // Header
- Padding(
- padding: EdgeInsets.only(
- left: HomeSpaceViewSizes.mHorizontalPadding,
- right: 8.0,
- top: Platform.isAndroid ? 8.0 : 0.0,
- ),
- child: MobileHomePageHeader(
- userProfile: widget.userProfile,
- ),
+ child: _HomePage(userProfile: widget.userProfile),
+ );
+ }
+
+ void _onLatestViewChange() async {
+ final id = getIt().latestOpenView?.id;
+ if (id == null) {
+ return;
+ }
+ await FolderEventSetLatestView(ViewIdPB(value: id)).send();
+ }
+}
+
+class _HomePage extends StatefulWidget {
+ const _HomePage({required this.userProfile});
+
+ final UserProfilePB userProfile;
+
+ @override
+ State<_HomePage> createState() => _HomePageState();
+}
+
+class _HomePageState extends State<_HomePage> {
+ Loading? loadingIndicator;
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocConsumer(
+ buildWhen: (previous, current) =>
+ previous.currentWorkspace?.workspaceId !=
+ current.currentWorkspace?.workspaceId,
+ listener: (context, state) {
+ getIt().reset();
+ mCurrentWorkspace.value = state.currentWorkspace;
+
+ Debounce.debounce(
+ 'workspace_action_result',
+ const Duration(milliseconds: 150),
+ () {
+ _showResultDialog(context, state);
+ },
+ );
+ },
+ builder: (context, state) {
+ if (state.currentWorkspace == null) {
+ return const SizedBox.shrink();
+ }
+
+ final workspaceId = state.currentWorkspace!.workspaceId;
+
+ return Column(
+ key: ValueKey('mobile_home_page_$workspaceId'),
+ children: [
+ // Header
+ Padding(
+ padding: const EdgeInsets.only(
+ left: HomeSpaceViewSizes.mHorizontalPadding,
+ right: 8.0,
+
+ ),
+ child: MobileHomePageHeader(
+ userProfile: widget.userProfile,
),
+ ),
- Expanded(
- child: BlocProvider(
- create: (context) =>
- SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),
- child: MobileSpaceTab(
- userProfile: widget.userProfile,
+ Expanded(
+ child: MultiBlocProvider(
+ providers: [
+ BlocProvider(
+ create: (_) =>
+ SpaceOrderBloc()..add(const SpaceOrderEvent.initial()),
+ ),
+ BlocProvider(
+ create: (_) => SidebarSectionsBloc()
+ ..add(
+ SidebarSectionsEvent.initial(
+ widget.userProfile,
+ workspaceId,
+ ),
+ ),
),
+ BlocProvider(
+ create: (_) =>
+ FavoriteBloc()..add(const FavoriteEvent.initial()),
+ ),
+ BlocProvider(
+ create: (_) => SpaceBloc()
+ ..add(
+ SpaceEvent.initial(
+ widget.userProfile,
+ workspaceId,
+ openFirstPage: false,
+ ),
+ ),
+ ),
+ ],
+ child: MobileSpaceTab(
+ userProfile: widget.userProfile,
),
),
- ],
- );
- },
- ),
+ ),
+ ],
+ );
+ },
);
}
- void _onLatestViewChange() async {
- final id = getIt().latestOpenView?.id;
- if (id == null) {
+ void _showResultDialog(BuildContext context, UserWorkspaceState state) {
+ final actionResult = state.actionResult;
+ if (actionResult == null) {
return;
}
- await FolderEventSetLatestView(ViewIdPB(value: id)).send();
+
+ Log.info('workspace action result: $actionResult');
+
+ final actionType = actionResult.actionType;
+ final result = actionResult.result;
+ final isLoading = actionResult.isLoading;
+
+ if (isLoading) {
+ loadingIndicator ??= Loading(context)..start();
+ return;
+ } else {
+ loadingIndicator?.stop();
+ loadingIndicator = null;
+ }
+
+ if (result == null) {
+ return;
+ }
+
+ result.onFailure((f) {
+ Log.error(
+ '[Workspace] Failed to perform ${actionType.toString()} action: $f',
+ );
+ });
+
+ final String? message;
+ ToastificationType toastType = ToastificationType.success;
+ switch (actionType) {
+ case UserWorkspaceActionType.open:
+ message = result.fold(
+ (s) {
+ toastType = ToastificationType.success;
+ return LocaleKeys.workspace_openSuccess.tr();
+ },
+ (e) {
+ toastType = ToastificationType.error;
+ return '${LocaleKeys.workspace_openFailed.tr()}: ${e.msg}';
+ },
+ );
+ break;
+
+ default:
+ message = null;
+ toastType = ToastificationType.error;
+ break;
+ }
+
+ if (message != null) {
+ showToastNotification(context, message: message, type: toastType);
+ }
}
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
index 28d0915aefc65..03d444e0b9035 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_page_header.dart
@@ -1,10 +1,10 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
-import 'package:appflowy/mobile/presentation/home/mobile_home_setting_page.dart';
import 'package:appflowy/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart';
import 'package:appflowy/plugins/base/emoji/emoji_picker_screen.dart';
-import 'package:appflowy/plugins/base/icon/icon_picker.dart';
+import 'package:appflowy/shared/icon_emoji_picker/flowy_icon_emoji_picker.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/util/built_in_svgs.dart';
import 'package:appflowy/workspace/application/user/settings_user_bloc.dart';
@@ -17,6 +17,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
+import 'setting/settings_popup_menu.dart';
+
class MobileHomePageHeader extends StatelessWidget {
const MobileHomePageHeader({
super.key,
@@ -44,15 +46,10 @@ class MobileHomePageHeader extends StatelessWidget {
? _MobileWorkspace(userProfile: userProfile)
: _MobileUser(userProfile: userProfile),
),
- GestureDetector(
- onTap: () => context.push(
- MobileHomeSettingPage.routeName,
- ),
- child: const Padding(
- padding: EdgeInsets.all(8.0),
- child: FlowySvg(FlowySvgs.m_setting_m),
- ),
+ HomePageSettingsPopupMenu(
+ userProfile: userProfile,
),
+ const HSpace(8.0),
],
),
);
@@ -113,8 +110,10 @@ class _MobileWorkspace extends StatelessWidget {
if (currentWorkspace == null) {
return const SizedBox.shrink();
}
- return GestureDetector(
- onTap: () {
+ return AnimatedGestureDetector(
+ scaleFactor: 0.99,
+ alignment: Alignment.centerLeft,
+ onTapUp: () {
context.read().add(
const UserWorkspaceEvent.fetchWorkspaces(),
);
@@ -130,6 +129,7 @@ class _MobileWorkspace extends StatelessWidget {
fontSize: 16.0,
enableEdit: false,
alignment: Alignment.centerLeft,
+ figmaLineHeight: 16.0,
onSelected: (result) => context.read().add(
UserWorkspaceEvent.updateWorkspaceIcon(
currentWorkspace.workspaceId,
@@ -143,7 +143,7 @@ class _MobileWorkspace extends StatelessWidget {
: const HSpace(8),
FlowyText.semibold(
currentWorkspace.name,
- fontSize: 16.0,
+ fontSize: 20.0,
overflow: TextOverflow.ellipsis,
),
],
@@ -162,9 +162,10 @@ class _MobileWorkspace extends StatelessWidget {
showHeader: true,
showDragHandle: true,
showCloseButton: true,
+ useRootNavigator: true,
title: LocaleKeys.workspace_menuTitle.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
- builder: (_) {
+ builder: (sheetContext) {
return BlocProvider.value(
value: context.read(),
child: BlocBuilder(
@@ -179,7 +180,7 @@ class _MobileWorkspace extends StatelessWidget {
currentWorkspace: currentWorkspace,
workspaces: workspaces,
onWorkspaceSelected: (workspace) {
- context.pop();
+ Navigator.of(sheetContext).pop();
if (workspace == currentWorkspace) {
return;
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart
index 964f9e5aa5094..1e0ddb5a5163f 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_setting_page.dart
@@ -5,6 +5,7 @@ import 'package:appflowy/mobile/presentation/base/app_bar/app_bar.dart';
import 'package:appflowy/mobile/presentation/presentation.dart';
import 'package:appflowy/mobile/presentation/setting/cloud/cloud_setting_group.dart';
import 'package:appflowy/mobile/presentation/setting/user_session_setting_group.dart';
+import 'package:appflowy/mobile/presentation/setting/workspace/workspace_setting_group.dart';
import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
import 'package:appflowy/startup/startup.dart';
import 'package:appflowy/user/application/auth/auth_service.dart';
@@ -79,14 +80,14 @@ class _MobileHomeSettingPageState extends State {
PersonalInfoSettingGroup(
userProfile: userProfile,
),
- // TODO: Enable and implement along with Push Notifications
- // const NotificationsSettingGroup(),
+ const WorkspaceSettingGroup(),
const AppearanceSettingGroup(),
const LanguageSettingGroup(),
if (Env.enableCustomCloud) const CloudSettingGroup(),
const SupportSettingGroup(),
const AboutSettingGroup(),
UserSessionSettingGroup(
+ userProfile: userProfile,
showThirdPartyLogin: showThirdPartyLogin,
),
const VSpace(20),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart
index 2ee9e14175685..73a5381d42c2c 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/mobile_home_trash_page.dart
@@ -64,11 +64,7 @@ class MobileHomeTrashPage extends StatelessWidget {
],
),
body: state.objects.isEmpty
- ? FlowyMobileStateContainer.info(
- emoji: '🗑️',
- title: LocaleKeys.trash_mobile_empty.tr(),
- description: LocaleKeys.trash_mobile_emptyDescription.tr(),
- )
+ ? const _EmptyTrashBin()
: _DeletedFilesListView(state),
);
},
@@ -82,6 +78,41 @@ enum _TrashActionType {
deleteAll,
}
+class _EmptyTrashBin extends StatelessWidget {
+ const _EmptyTrashBin();
+
+ @override
+ Widget build(BuildContext context) {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const FlowySvg(
+ FlowySvgs.m_empty_trash_xl,
+ size: Size.square(46),
+ ),
+ const VSpace(16.0),
+ FlowyText.medium(
+ LocaleKeys.trash_mobile_empty.tr(),
+ fontSize: 18.0,
+ textAlign: TextAlign.center,
+ ),
+ const VSpace(8.0),
+ FlowyText.regular(
+ LocaleKeys.trash_mobile_emptyDescription.tr(),
+ fontSize: 17.0,
+ maxLines: 10,
+ textAlign: TextAlign.center,
+ lineHeight: 1.3,
+ color: Theme.of(context).hintColor,
+ ),
+ const VSpace(kBottomNavigationBarHeight + 36.0),
+ ],
+ ),
+ );
+ }
+}
+
class _TrashActionAllButton extends StatelessWidget {
/// Switch between 'delete all' and 'restore all' feature
const _TrashActionAllButton({
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart
index f3e807ec9cbeb..c0baa641d9bb0 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/recent_folder/recent_space.dart
@@ -68,38 +68,34 @@ class _RecentViews extends StatelessWidget {
? const Color(0xFFE9E9EC)
: const Color(0x1AFFFFFF);
return SlidableAutoCloseBehavior(
- child: Scrollbar(
- child: ListView.separated(
- key: const PageStorageKey('recent_views_page_storage_key'),
- padding: EdgeInsets.only(
- left: HomeSpaceViewSizes.mHorizontalPadding,
- right: HomeSpaceViewSizes.mHorizontalPadding,
- bottom: HomeSpaceViewSizes.mVerticalPadding +
- MediaQuery.of(context).padding.bottom,
- ),
- itemBuilder: (context, index) {
- final sectionView = recentViews[index];
- return Container(
- padding: const EdgeInsets.symmetric(vertical: 24.0),
- decoration: BoxDecoration(
- border: Border(
- bottom: BorderSide(
- color: borderColor,
- width: 0.5,
- ),
+ child: ListView.separated(
+ key: const PageStorageKey('recent_views_page_storage_key'),
+ padding: EdgeInsets.only(
+ bottom: HomeSpaceViewSizes.mVerticalPadding +
+ MediaQuery.of(context).padding.bottom,
+ ),
+ itemBuilder: (context, index) {
+ final sectionView = recentViews[index];
+ return Container(
+ padding: const EdgeInsets.symmetric(vertical: 24.0),
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: borderColor,
+ width: 0.5,
),
),
- child: MobileViewPage(
- key: ValueKey(sectionView.item.id),
- view: sectionView.item,
- timestamp: sectionView.timestamp,
- type: MobilePageCardType.recent,
- ),
- );
- },
- separatorBuilder: (context, index) => const HSpace(8),
- itemCount: recentViews.length,
- ),
+ ),
+ child: MobileViewPage(
+ key: ValueKey(sectionView.item.id),
+ view: sectionView.item,
+ timestamp: sectionView.timestamp,
+ type: MobilePageCardType.recent,
+ ),
+ );
+ },
+ separatorBuilder: (context, index) => const HSpace(8),
+ itemCount: recentViews.length,
),
);
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart
index 4cd212f5a9ca1..d610b4452b9d7 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder.dart
@@ -1,6 +1,6 @@
import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
-import 'package:appflowy/mobile/presentation/bottom_sheet/default_mobile_action_pane.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/workspace/application/menu/sidebar_sections_bloc.dart';
@@ -43,47 +43,17 @@ class MobileSectionFolder extends StatelessWidget {
onPressed: () => context
.read()
.add(const FolderEvent.expandOrUnExpand()),
- onAdded: () {
- context.read().add(
- SidebarSectionsEvent.createRootViewInSection(
- name: LocaleKeys.menuAppHeader_defaultNewPageName
- .tr(),
- index: 0,
- viewSection: spaceType.toViewSectionPB,
- ),
- );
- context.read().add(
- const FolderEvent.expandOrUnExpand(isExpanded: true),
- );
- },
+ onAdded: () => _createNewPage(context),
),
),
if (state.isExpanded)
- ...views.map(
- (view) => MobileViewItem(
- key: ValueKey(
- '${FolderSpaceType.private.name} ${view.id}',
- ),
+ Padding(
+ padding: const EdgeInsets.only(
+ left: HomeSpaceViewSizes.leftPadding,
+ ),
+ child: _Pages(
+ views: views,
spaceType: spaceType,
- isFirstChild: view.id == views.first.id,
- view: view,
- level: 0,
- leftPadding: HomeSpaceViewSizes.leftPadding,
- isFeedback: false,
- onSelected: context.pushView,
- endActionPane: (context) {
- final view = context.read().state.view;
- return buildEndActionPane(
- context,
- [
- MobilePaneActionType.more,
- if (view.layout == ViewLayoutPB.Document)
- MobilePaneActionType.add,
- ],
- spaceType: spaceType,
- needSpace: false,
- );
- },
),
),
],
@@ -92,4 +62,63 @@ class MobileSectionFolder extends StatelessWidget {
),
);
}
+
+ void _createNewPage(BuildContext context) {
+ context.read().add(
+ SidebarSectionsEvent.createRootViewInSection(
+ name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ index: 0,
+ viewSection: spaceType.toViewSectionPB,
+ ),
+ );
+ context.read().add(
+ const FolderEvent.expandOrUnExpand(isExpanded: true),
+ );
+ }
+}
+
+class _Pages extends StatelessWidget {
+ const _Pages({
+ required this.views,
+ required this.spaceType,
+ });
+
+ final List views;
+ final FolderSpaceType spaceType;
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: views
+ .map(
+ (view) => MobileViewItem(
+ key: ValueKey(
+ '${FolderSpaceType.private.name} ${view.id}',
+ ),
+ spaceType: spaceType,
+ isFirstChild: view.id == views.first.id,
+ view: view,
+ level: 0,
+ leftPadding: HomeSpaceViewSizes.leftPadding,
+ isFeedback: false,
+ onSelected: context.pushView,
+ endActionPane: (context) {
+ final view = context.read().state.view;
+ return buildEndActionPane(
+ context,
+ [
+ MobilePaneActionType.more,
+ if (view.layout == ViewLayoutPB.Document)
+ MobilePaneActionType.add,
+ ],
+ spaceType: spaceType,
+ needSpace: false,
+ spaceRatio: 5,
+ );
+ },
+ ),
+ )
+ .toList(),
+ );
+ }
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart
index aa84dc7601727..b1d2bf690925a 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/section_folder/mobile_home_section_folder_header.dart
@@ -32,6 +32,7 @@ class _MobileSectionFolderHeaderState extends State {
Widget build(BuildContext context) {
return Row(
children: [
+ const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
Expanded(
child: FlowyButton(
text: FlowyText.medium(
@@ -57,15 +58,19 @@ class _MobileSectionFolderHeaderState extends State {
},
),
),
- FlowyIconButton(
- key: mobileCreateNewPageButtonKey,
- hoverColor: Theme.of(context).colorScheme.secondaryContainer,
- height: HomeSpaceViewSizes.mViewButtonDimension,
- width: HomeSpaceViewSizes.mViewButtonDimension,
- icon: const FlowySvg(
- FlowySvgs.m_space_add_s,
+ GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onTap: widget.onAdded,
+ child: Container(
+ // expand the touch area
+ margin: const EdgeInsets.symmetric(
+ horizontal: HomeSpaceViewSizes.mHorizontalPadding,
+ vertical: 8.0,
+ ),
+ child: const FlowySvg(
+ FlowySvgs.m_space_add_s,
+ ),
),
- onPressed: widget.onAdded,
),
],
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart
new file mode 100644
index 0000000000000..f5afc10465b8d
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/setting/settings_popup_menu.dart
@@ -0,0 +1,155 @@
+import 'package:appflowy/core/helpers/url_launcher.dart';
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/presentation.dart';
+import 'package:appflowy/mobile/presentation/setting/workspace/invite_members_screen.dart';
+import 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart'
+ hide PopupMenuButton, PopupMenuDivider, PopupMenuItem, PopupMenuEntry;
+import 'package:go_router/go_router.dart';
+
+enum _MobileSettingsPopupMenuItem {
+ settings,
+ members,
+ trash,
+ help,
+}
+
+class HomePageSettingsPopupMenu extends StatelessWidget {
+ const HomePageSettingsPopupMenu({
+ super.key,
+ required this.userProfile,
+ });
+
+ final UserProfilePB userProfile;
+
+ @override
+ Widget build(BuildContext context) {
+
+ return PopupMenuButton<_MobileSettingsPopupMenuItem>(
+ offset: const Offset(0, 36),
+ padding: EdgeInsets.zero,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(
+ Radius.circular(12.0),
+ ),
+ ),
+ shadowColor: const Color(0x68000000),
+ elevation: 10,
+ color: context.popupMenuBackgroundColor,
+ itemBuilder: (BuildContext context) =>
+ >[
+ _buildItem(
+ value: _MobileSettingsPopupMenuItem.settings,
+ svg: FlowySvgs.m_notification_settings_s,
+ text: LocaleKeys.settings_popupMenuItem_settings.tr(),
+ ),
+ // only show the member items in cloud mode
+ if (userProfile.authenticator == AuthenticatorPB.AppFlowyCloud) ...[
+ const PopupMenuDivider(height: 0.5),
+ _buildItem(
+ value: _MobileSettingsPopupMenuItem.members,
+ svg: FlowySvgs.m_settings_member_s,
+ text: LocaleKeys.settings_popupMenuItem_members.tr(),
+ ),
+ ],
+ const PopupMenuDivider(height: 0.5),
+ _buildItem(
+ value: _MobileSettingsPopupMenuItem.trash,
+ svg: FlowySvgs.trash_s,
+ text: LocaleKeys.settings_popupMenuItem_trash.tr(),
+ ),
+ const PopupMenuDivider(height: 0.5),
+ _buildItem(
+ value: _MobileSettingsPopupMenuItem.help,
+ svg: FlowySvgs.message_support_s,
+ text: LocaleKeys.settings_popupMenuItem_helpAndSupport.tr(),
+ ),
+ ],
+ onSelected: (_MobileSettingsPopupMenuItem value) {
+ switch (value) {
+ case _MobileSettingsPopupMenuItem.members:
+ _openMembersPage(context);
+ break;
+ case _MobileSettingsPopupMenuItem.trash:
+ _openTrashPage(context);
+ break;
+ case _MobileSettingsPopupMenuItem.settings:
+ _openSettingsPage(context);
+ break;
+ case _MobileSettingsPopupMenuItem.help:
+ _openHelpPage(context);
+ break;
+ }
+ },
+ child: const Padding(
+ padding: EdgeInsets.all(8.0),
+ child: FlowySvg(
+ FlowySvgs.m_settings_more_s,
+ ),
+ ),
+ );
+ }
+
+ PopupMenuItem _buildItem({
+ required T value,
+ required FlowySvgData svg,
+ required String text,
+ }) {
+ return PopupMenuItem(
+ value: value,
+ padding: EdgeInsets.zero,
+ child: _PopupButton(
+ svg: svg,
+ text: text,
+ ),
+ );
+ }
+
+ void _openMembersPage(BuildContext context) {
+ context.push(InviteMembersScreen.routeName);
+ }
+
+ void _openTrashPage(BuildContext context) {
+ context.push(MobileHomeTrashPage.routeName);
+ }
+
+ void _openHelpPage(BuildContext context) {
+ afLaunchUrlString('https://discord.com/invite/9Q2xaN37tV');
+ }
+
+ void _openSettingsPage(BuildContext context) {
+ context.push(MobileHomeSettingPage.routeName);
+ }
+}
+
+class _PopupButton extends StatelessWidget {
+ const _PopupButton({
+ required this.svg,
+ required this.text,
+ });
+
+ final FlowySvgData svg;
+ final String text;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 44,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ child: Row(
+ children: [
+ FlowySvg(svg, size: const Size.square(20)),
+ const HSpace(12),
+ FlowyText.regular(
+ text,
+ fontSize: 16,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart
index 341acb8099d67..2de40600f2cc8 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/empty_placeholder.dart
@@ -6,7 +6,10 @@ import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
class EmptySpacePlaceholder extends StatelessWidget {
- const EmptySpacePlaceholder({super.key, required this.type});
+ const EmptySpacePlaceholder({
+ super.key,
+ required this.type,
+ });
final MobilePageCardType type;
@@ -36,6 +39,7 @@ class EmptySpacePlaceholder extends StatelessWidget {
lineHeight: 1.3,
color: Theme.of(context).hintColor,
),
+ const VSpace(kBottomNavigationBarHeight + 36.0),
],
),
);
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart
index 71be20453dd96..f5e031666af8d 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/shared/mobile_page_card.dart
@@ -5,6 +5,7 @@ import 'package:appflowy/generated/locale_keys.g.dart';
import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
import 'package:appflowy/mobile/application/recent/recent_view_bloc.dart';
+import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
import 'package:appflowy/plugins/document/presentation/editor_plugins/plugins.dart';
import 'package:appflowy/shared/appflowy_network_image.dart';
@@ -15,6 +16,7 @@ import 'package:appflowy/workspace/application/settings/appearance/appearance_cu
import 'package:appflowy/workspace/application/settings/date_time/date_format_ext.dart';
import 'package:appflowy/workspace/application/settings/date_time/time_format_ext.dart';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
+import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
import 'package:appflowy_editor/appflowy_editor.dart';
@@ -76,13 +78,14 @@ class MobileViewPage extends StatelessWidget {
: MobilePaneActionType.addToFavorites,
],
cardType: type,
+ spaceRatio: 4,
),
- child: GestureDetector(
- behavior: HitTestBehavior.opaque,
- onTapUp: (_) => context.pushView(view),
+ child: AnimatedGestureDetector(
+ onTapUp: () => context.pushView(view),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
+ const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
Expanded(child: _buildDescription(context, state)),
const HSpace(20.0),
SizedBox(
@@ -90,6 +93,7 @@ class MobileViewPage extends StatelessWidget {
height: 60,
child: _buildCover(context, state),
),
+ const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
],
),
),
@@ -211,7 +215,7 @@ class MobileViewPage extends StatelessWidget {
Widget _buildLastViewed(BuildContext context) {
final textColor = Theme.of(context).isLightMode
- ? const Color(0xFF171717)
+ ? const Color(0x7F171717)
: Colors.white.withOpacity(0.45);
if (timestamp == null) {
return const SizedBox.shrink();
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space.dart
index 48ef17e86c987..41f36f7b709e6 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space.dart
@@ -4,37 +4,21 @@ 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/mobile/presentation/presentation.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';
import 'package:appflowy/workspace/application/view/view_bloc.dart';
import 'package:appflowy/workspace/application/view/view_ext.dart';
import 'package:appflowy/workspace/presentation/home/home_sizes.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:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
-class MobileSpace extends StatefulWidget {
+class MobileSpace extends StatelessWidget {
const MobileSpace({super.key});
- @override
- State createState() => _MobileSpaceState();
-}
-
-class _MobileSpaceState extends State {
- @override
- void initState() {
- super.initState();
- createNewPageNotifier.addListener(_createNewPage);
- }
-
- @override
- void dispose() {
- createNewPageNotifier.removeListener(_createNewPage);
- super.dispose();
- }
-
@override
Widget build(BuildContext context) {
return BlocBuilder(
@@ -50,23 +34,17 @@ class _MobileSpaceState extends State {
MobileSpaceHeader(
isExpanded: state.isExpanded,
space: currentSpace,
- onAdded: () {
- context.read().add(
- SpaceEvent.createPage(
- name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
- layout: ViewLayoutPB.Document,
- index: 0,
- ),
- );
- context.read().add(
- SpaceEvent.expand(currentSpace, true),
- );
- },
+ onAdded: () => _showCreatePageMenu(context, currentSpace),
onPressed: () => _showSpaceMenu(context),
),
- _Pages(
- key: ValueKey(currentSpace.id),
- space: currentSpace,
+ Padding(
+ padding: const EdgeInsets.only(
+ left: HomeSpaceViewSizes.mHorizontalPadding,
+ ),
+ child: _Pages(
+ key: ValueKey(currentSpace.id),
+ space: currentSpace,
+ ),
),
],
);
@@ -82,6 +60,7 @@ class _MobileSpaceState extends State {
showDragHandle: true,
showCloseButton: true,
showDoneButton: true,
+ useRootNavigator: true,
title: LocaleKeys.space_title.tr(),
backgroundColor: Theme.of(context).colorScheme.surface,
builder: (_) {
@@ -96,13 +75,36 @@ class _MobileSpaceState extends State {
);
}
- void _createNewPage() {
- context.read().add(
- SpaceEvent.createPage(
- name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
- layout: ViewLayoutPB.Document,
- ),
+ void _showCreatePageMenu(BuildContext context, ViewPB space) {
+ final title = space.name;
+ showMobileBottomSheet(
+ context,
+ showHeader: true,
+ title: title,
+ showDragHandle: true,
+ showCloseButton: true,
+ useRootNavigator: true,
+ showDivider: false,
+ backgroundColor: Theme.of(context).colorScheme.surface,
+ builder: (sheetContext) {
+ return AddNewPageWidgetBottomSheet(
+ view: space,
+ onAction: (layout) {
+ Navigator.of(sheetContext).pop();
+ context.read().add(
+ SpaceEvent.createPage(
+ name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ layout: layout,
+ index: 0,
+ ),
+ );
+ context.read().add(
+ SpaceEvent.expand(space, true),
+ );
+ },
);
+ },
+ );
}
}
@@ -124,8 +126,15 @@ class _Pages extends StatelessWidget {
final spaceType = space.spacePermission == SpacePermission.publicToAll
? FolderSpaceType.public
: FolderSpaceType.private;
+ final childViews = state.view.childViews.unique((view) => view.id);
+ if (childViews.length != state.view.childViews.length) {
+ final duplicatedViews = state.view.childViews
+ .where((view) => childViews.contains(view))
+ .toList();
+ Log.error('some view id are duplicated: $duplicatedViews');
+ }
return Column(
- children: state.view.childViews
+ children: childViews
.map(
(view) => MobileViewItem(
key: ValueKey(
@@ -140,15 +149,16 @@ class _Pages extends StatelessWidget {
onSelected: context.pushView,
endActionPane: (context) {
final view = context.read().state.view;
+ final actions = [
+ MobilePaneActionType.more,
+ if (view.layout == ViewLayoutPB.Document)
+ MobilePaneActionType.add,
+ ];
return buildEndActionPane(
context,
- [
- MobilePaneActionType.more,
- if (view.layout == ViewLayoutPB.Document)
- MobilePaneActionType.add,
- ],
+ actions,
spaceType: spaceType,
- needSpace: false,
+ spaceRatio: actions.length == 1 ? 3 : 4,
);
},
),
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_header.dart
index ffc16914048ae..0cd80ff1bbf3a 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_header.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_header.dart
@@ -1,4 +1,5 @@
import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/workspace/presentation/home/home_sizes.dart';
import 'package:appflowy/workspace/presentation/home/menu/sidebar/space/space_icon.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
@@ -30,9 +31,12 @@ class MobileSpaceHeader extends StatelessWidget {
height: 48,
child: Row(
children: [
+ const HSpace(HomeSpaceViewSizes.mHorizontalPadding),
SpaceIcon(
dimension: 24,
space: space,
+ svgSize: 14,
+ textDimension: 18.0,
cornerRadius: 6.0,
),
const HSpace(8),
@@ -49,8 +53,15 @@ class MobileSpaceHeader extends StatelessWidget {
GestureDetector(
behavior: HitTestBehavior.translucent,
onTap: onAdded,
- child: const FlowySvg(
- FlowySvgs.m_space_add_s,
+ child: Container(
+ // expand the touch area
+ margin: const EdgeInsets.symmetric(
+ horizontal: HomeSpaceViewSizes.mHorizontalPadding,
+ vertical: 8.0,
+ ),
+ child: const FlowySvg(
+ FlowySvgs.m_space_add_s,
+ ),
),
),
],
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart
index 6788e1396d4cb..2fe580cecd68d 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/space/mobile_space_menu.dart
@@ -59,6 +59,7 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
children: [
FlowyText.medium(
space.name,
+ fontSize: 16.0,
),
const HSpace(6.0),
if (space.spacePermission == SpacePermission.private)
@@ -68,16 +69,19 @@ class _SidebarSpaceMenuItem extends StatelessWidget {
),
],
),
+ margin: const EdgeInsets.symmetric(horizontal: 12.0),
iconPadding: 10,
leftIcon: SpaceIcon(
dimension: 24,
space: space,
+ svgSize: 14,
+ textDimension: 18.0,
cornerRadius: 6.0,
),
- leftIconSize: const Size.square(20),
+ leftIconSize: const Size.square(24),
rightIcon: isSelected
? const FlowySvg(
- FlowySvgs.workspace_selected_s,
+ FlowySvgs.m_blue_check_s,
blendMode: null,
)
: null,
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart
index 5602f46f895df..f8c9a0d3b11ad 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/_tab_bar.dart
@@ -21,12 +21,14 @@ class MobileSpaceTabBar extends StatelessWidget {
Widget build(BuildContext context) {
final baseStyle = Theme.of(context).textTheme.bodyMedium;
final labelStyle = baseStyle?.copyWith(
- fontWeight: FontWeight.w600,
+ fontWeight: FontWeight.w500,
fontSize: 16.0,
+ height: 22.0 / 16.0,
);
final unselectedLabelStyle = baseStyle?.copyWith(
fontWeight: FontWeight.w400,
fontSize: 15.0,
+ height: 22.0 / 15.0,
);
return Container(
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart
new file mode 100644
index 0000000000000..a5cb39ffaa6fd
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/ai_bubble_button.dart
@@ -0,0 +1,81 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
+import 'package:appflowy/mobile/presentation/home/tab/mobile_space_tab.dart';
+import 'package:appflowy/util/theme_extension.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+
+class FloatingAIEntry extends StatelessWidget {
+ const FloatingAIEntry({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return AnimatedGestureDetector(
+ scaleFactor: 0.99,
+ onTapUp: () => mobileCreateNewAIChatNotifier.value =
+ mobileCreateNewAIChatNotifier.value + 1,
+ child: DecoratedBox(
+ decoration: _buildShadowDecoration(context),
+ child: Container(
+ decoration: _buildWrapperDecoration(context),
+ height: 48,
+ alignment: Alignment.centerLeft,
+ child: Padding(
+ padding: const EdgeInsets.only(left: 18),
+ child: _buildHintText(context),
+ ),
+ ),
+ ),
+ );
+ }
+
+ BoxDecoration _buildShadowDecoration(BuildContext context) {
+ return BoxDecoration(
+ borderRadius: BorderRadius.circular(30),
+ boxShadow: [
+ BoxShadow(
+ blurRadius: 20,
+ spreadRadius: 1,
+ offset: const Offset(0, 4),
+ color: Colors.black.withOpacity(0.05),
+ ),
+ ],
+ );
+ }
+
+ BoxDecoration _buildWrapperDecoration(BuildContext context) {
+ final outlineColor = Theme.of(context).colorScheme.outline;
+ final borderColor = Theme.of(context).isLightMode
+ ? outlineColor.withOpacity(0.7)
+ : outlineColor.withOpacity(0.3);
+ return BoxDecoration(
+ borderRadius: BorderRadius.circular(30),
+ color: Theme.of(context).colorScheme.surface,
+ border: Border.fromBorderSide(
+ BorderSide(
+ color: borderColor,
+ ),
+ ),
+ );
+ }
+
+ Widget _buildHintText(BuildContext context) {
+ return Row(
+ children: [
+ FlowySvg(
+ FlowySvgs.toolbar_item_ai_s,
+ size: const Size.square(16.0),
+ color: Theme.of(context).hintColor,
+ opacity: 0.7,
+ ),
+ const HSpace(8),
+ FlowyText(
+ LocaleKeys.chat_inputMessageHint.tr(),
+ color: Theme.of(context).hintColor,
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart
index 097bd22910826..8181e0a10aa3e 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/tab/mobile_space_tab.dart
@@ -1,16 +1,34 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/application/mobile_router.dart';
import 'package:appflowy/mobile/presentation/home/favorite_folder/favorite_space.dart';
import 'package:appflowy/mobile/presentation/home/home_space/home_space.dart';
import 'package:appflowy/mobile/presentation/home/recent_folder/recent_space.dart';
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/workspace/application/menu/sidebar_sections_bloc.dart';
+import 'package:appflowy/workspace/application/sidebar/folder/folder_bloc.dart';
+import 'package:appflowy/workspace/application/sidebar/space/space_bloc.dart';
+import 'package:appflowy/workspace/application/user/user_workspace_bloc.dart';
+import 'package:appflowy_backend/log.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:easy_localization/easy_localization.dart';
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:provider/provider.dart';
+import 'ai_bubble_button.dart';
+
+final ValueNotifier mobileCreateNewAIChatNotifier = ValueNotifier(0);
+
class MobileSpaceTab extends StatefulWidget {
- const MobileSpaceTab({super.key, required this.userProfile});
+ const MobileSpaceTab({
+ super.key,
+ required this.userProfile,
+ });
final UserProfilePB userProfile;
@@ -22,10 +40,24 @@ class _MobileSpaceTabState extends State
with SingleTickerProviderStateMixin {
TabController? tabController;
+ @override
+ void initState() {
+ super.initState();
+
+ mobileCreateNewPageNotifier.addListener(_createNewDocument);
+ mobileCreateNewAIChatNotifier.addListener(_createNewAIChat);
+ mobileLeaveWorkspaceNotifier.addListener(_leaveWorkspace);
+ }
+
@override
void dispose() {
tabController?.removeListener(_onTabChange);
tabController?.dispose();
+
+ mobileCreateNewPageNotifier.removeListener(_createNewDocument);
+ mobileCreateNewAIChatNotifier.removeListener(_createNewAIChat);
+ mobileLeaveWorkspaceNotifier.removeListener(_leaveWorkspace);
+
super.dispose();
}
@@ -33,36 +65,60 @@ class _MobileSpaceTabState extends State
Widget build(BuildContext context) {
return Provider.value(
value: widget.userProfile,
- child: BlocBuilder(
- builder: (context, state) {
- if (state.isLoading) {
- return const SizedBox.shrink();
- }
+ child: MultiBlocListener(
+ listeners: [
+ BlocListener(
+ listenWhen: (p, c) =>
+ p.lastCreatedPage?.id != c.lastCreatedPage?.id,
+ listener: (context, state) {
+ final lastCreatedPage = state.lastCreatedPage;
+ if (lastCreatedPage != null) {
+ context.pushView(lastCreatedPage);
+ }
+ },
+ ),
+ BlocListener(
+ listenWhen: (p, c) =>
+ p.lastCreatedRootView?.id != c.lastCreatedRootView?.id,
+ listener: (context, state) {
+ final lastCreatedPage = state.lastCreatedRootView;
+ if (lastCreatedPage != null) {
+ context.pushView(lastCreatedPage);
+ }
+ },
+ ),
+ ],
+ child: BlocBuilder(
+ builder: (context, state) {
+ if (state.isLoading) {
+ return const SizedBox.shrink();
+ }
- _initTabController(state);
+ _initTabController(state);
- return Column(
- crossAxisAlignment: CrossAxisAlignment.start,
- children: [
- MobileSpaceTabBar(
- tabController: tabController!,
- tabs: state.tabsOrder,
- onReorder: (from, to) {
- context.read().add(
- SpaceOrderEvent.reorder(from, to),
- );
- },
- ),
- const HSpace(12.0),
- Expanded(
- child: TabBarView(
- controller: tabController,
- children: _buildTabs(state),
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ MobileSpaceTabBar(
+ tabController: tabController!,
+ tabs: state.tabsOrder,
+ onReorder: (from, to) {
+ context.read().add(
+ SpaceOrderEvent.reorder(from, to),
+ );
+ },
),
- ),
- ],
- );
- },
+ const HSpace(12.0),
+ Expanded(
+ child: TabBarView(
+ controller: tabController,
+ children: _buildTabs(state),
+ ),
+ ),
+ ],
+ );
+ },
+ ),
),
);
}
@@ -96,7 +152,20 @@ class _MobileSpaceTabState extends State
case MobileSpaceTabType.recent:
return const MobileRecentSpace();
case MobileSpaceTabType.spaces:
- return MobileHomeSpace(userProfile: widget.userProfile);
+ return Stack(
+ children: [
+ MobileHomeSpace(userProfile: widget.userProfile),
+ // only show ai chat button for cloud user
+ if (widget.userProfile.authenticator ==
+ AuthenticatorPB.AppFlowyCloud)
+ Positioned(
+ bottom: MediaQuery.of(context).padding.bottom + 16,
+ left: 20,
+ right: 20,
+ child: const FloatingAIEntry(),
+ ),
+ ],
+ );
case MobileSpaceTabType.favorites:
return MobileFavoriteSpace(userProfile: widget.userProfile);
default:
@@ -104,4 +173,45 @@ class _MobileSpaceTabState extends State
}
}).toList();
}
+
+ // quick create new page when clicking the add button in navigation bar
+ void _createNewDocument() {
+ _createNewPage(ViewLayoutPB.Document);
+ }
+
+ void _createNewAIChat() {
+ _createNewPage(ViewLayoutPB.Chat);
+ }
+
+ void _createNewPage(ViewLayoutPB layout) {
+ if (context.read().state.spaces.isNotEmpty) {
+ context.read().add(
+ SpaceEvent.createPage(
+ name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ layout: layout,
+ ),
+ );
+ } else if (layout == ViewLayoutPB.Document) {
+ // only support create document in section
+ context.read().add(
+ SidebarSectionsEvent.createRootViewInSection(
+ name: LocaleKeys.menuAppHeader_defaultNewPageName.tr(),
+ index: 0,
+ viewSection: FolderSpaceType.public.toViewSectionPB,
+ ),
+ );
+ }
+ }
+
+ void _leaveWorkspace() {
+ final workspaceId =
+ context.read().state.currentWorkspace?.workspaceId;
+ if (workspaceId == null) {
+ Log.error('Workspace ID is null');
+ return;
+ }
+ context
+ .read()
+ .add(UserWorkspaceEvent.leaveWorkspace(workspaceId));
+ }
}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart
index 17962bcc3c265..53d28387e95fa 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/home/workspaces/workspace_menu_bottom_sheet.dart
@@ -138,17 +138,21 @@ class _WorkspaceMenuItem extends StatelessWidget {
height: 60,
showTopBorder: showTopBorder,
showBottomBorder: false,
- leftIcon: WorkspaceIcon(
- enableEdit: false,
- iconSize: 26,
- fontSize: 16.0,
- workspace: workspace,
- onSelected: (result) => context.read().add(
- UserWorkspaceEvent.updateWorkspaceIcon(
- workspace.workspaceId,
- result.emoji,
+ leftIcon: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 4.0),
+ child: WorkspaceIcon(
+ enableEdit: false,
+ iconSize: 26,
+ fontSize: 16.0,
+ figmaLineHeight: 16.0,
+ workspace: workspace,
+ onSelected: (result) => context.read().add(
+ UserWorkspaceEvent.updateWorkspaceIcon(
+ workspace.workspaceId,
+ result.emoji,
+ ),
),
- ),
+ ),
),
trailing: workspace.workspaceId == currentWorkspace.workspaceId
? const FlowySvg(
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart
index 5d33dfa500551..1de6c568d3067 100644
--- a/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/mobile_bottom_navigation_bar.dart
@@ -1,14 +1,33 @@
+import 'dart:io';
import 'dart:ui';
import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
+import 'package:appflowy/mobile/presentation/widgets/navigation_bar_button.dart';
+import 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';
+import 'package:appflowy/shared/red_dot.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
import 'package:appflowy/util/theme_extension.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.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/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
-final PropertyValueNotifier createNewPageNotifier =
+enum BottomNavigationBarActionType {
+ home,
+ notificationMultiSelect,
+}
+
+final PropertyValueNotifier mobileCreateNewPageNotifier =
PropertyValueNotifier(null);
+final ValueNotifier bottomNavigationBarType =
+ ValueNotifier(BottomNavigationBarActionType.home);
const _homeLabel = 'home';
const _addLabel = 'add';
@@ -25,16 +44,16 @@ final _items = [
),
const BottomNavigationBarItem(
label: _notificationLabel,
- icon: FlowySvg(FlowySvgs.m_home_notification_m),
- activeIcon: FlowySvg(
- FlowySvgs.m_home_notification_m,
+ icon: _NotificationNavigationBarItemIcon(),
+ activeIcon: _NotificationNavigationBarItemIcon(
+ isActive: true,
),
),
];
/// Builds the "shell" for the app by building a Scaffold with a
/// BottomNavigationBar, where [child] is placed in the body of the Scaffold.
-class MobileBottomNavigationBar extends StatelessWidget {
+class MobileBottomNavigationBar extends StatefulWidget {
/// Constructs an [MobileBottomNavigationBar].
const MobileBottomNavigationBar({
required this.navigationShell,
@@ -44,30 +63,140 @@ class MobileBottomNavigationBar extends StatelessWidget {
/// The navigation shell and container for the branch Navigators.
final StatefulNavigationShell navigationShell;
+ @override
+ State createState() =>
+ _MobileBottomNavigationBarState();
+}
+
+class _MobileBottomNavigationBarState extends State {
+ Widget? _bottomNavigationBar;
+
+ @override
+ void initState() {
+ super.initState();
+
+ bottomNavigationBarType.addListener(_animate);
+ }
+
+ @override
+ void dispose() {
+ bottomNavigationBarType.removeListener(_animate);
+ super.dispose();
+ }
+
@override
Widget build(BuildContext context) {
- final isLightMode = Theme.of(context).isLightMode;
- final backgroundColor = isLightMode
- ? Colors.white.withOpacity(0.95)
- : const Color(0xFF23262B).withOpacity(0.95);
+ _bottomNavigationBar = switch (bottomNavigationBarType.value) {
+ BottomNavigationBarActionType.home =>
+ _buildHomePageNavigationBar(context),
+ BottomNavigationBarActionType.notificationMultiSelect =>
+ _buildNotificationNavigationBar(context),
+ };
+
return Scaffold(
- body: navigationShell,
+ body: widget.navigationShell,
extendBody: true,
- bottomNavigationBar: ClipRRect(
- child: BackdropFilter(
- filter: ImageFilter.blur(
- sigmaX: 3,
- sigmaY: 3,
- ),
- child: DecoratedBox(
- decoration: BoxDecoration(
- border: isLightMode
- ? Border(
- top: BorderSide(color: Theme.of(context).dividerColor),
+ bottomNavigationBar: AnimatedSwitcher(
+ duration: const Duration(milliseconds: 250),
+ switchInCurve: Curves.easeInOut,
+ switchOutCurve: Curves.easeInOut,
+ transitionBuilder: _transitionBuilder,
+ child: _bottomNavigationBar,
+ ),
+ );
+ }
+
+ Widget _buildHomePageNavigationBar(BuildContext context) {
+ return _HomePageNavigationBar(
+ navigationShell: widget.navigationShell,
+ );
+ }
+
+ Widget _buildNotificationNavigationBar(BuildContext context) {
+ return const _NotificationNavigationBar();
+ }
+
+ // widget A going down, widget B going up
+ Widget _transitionBuilder(
+ Widget child,
+ Animation animation,
+ ) {
+ return SlideTransition(
+ position: Tween(
+ begin: const Offset(0, 1),
+ end: Offset.zero,
+ ).animate(animation),
+ child: child,
+ );
+ }
+
+ void _animate() {
+ setState(() {});
+ }
+}
+
+class _NotificationNavigationBarItemIcon extends StatelessWidget {
+ const _NotificationNavigationBarItemIcon({
+ this.isActive = false,
+ });
+
+ final bool isActive;
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider.value(
+ value: getIt(),
+ child: BlocBuilder(
+ builder: (context, state) {
+ final hasUnreads = state.reminders.any(
+ (reminder) => !reminder.isRead,
+ );
+ return Stack(
+ children: [
+ isActive
+ ? const FlowySvg(
+ FlowySvgs.m_home_active_notification_m,
+ blendMode: null,
)
- : null,
- color: backgroundColor,
- ),
+ : const FlowySvg(
+ FlowySvgs.m_home_notification_m,
+ ),
+ if (hasUnreads)
+ const Positioned(
+ top: 2,
+ right: 4,
+ child: NotificationRedDot(),
+ ),
+ ],
+ );
+ },
+ ),
+ );
+ }
+}
+
+class _HomePageNavigationBar extends StatelessWidget {
+ const _HomePageNavigationBar({
+ required this.navigationShell,
+ });
+
+ final StatefulNavigationShell navigationShell;
+
+ @override
+ Widget build(BuildContext context) {
+ return ClipRRect(
+ child: BackdropFilter(
+ filter: ImageFilter.blur(
+ sigmaX: 3,
+ sigmaY: 3,
+ ),
+ child: DecoratedBox(
+ decoration: BoxDecoration(
+ border: context.border,
+ color: context.backgroundColor,
+ ),
+ child: Theme(
+ data: _getThemeData(context),
child: BottomNavigationBar(
showSelectedLabels: false,
showUnselectedLabels: false,
@@ -85,13 +214,32 @@ class MobileBottomNavigationBar extends StatelessWidget {
);
}
+ ThemeData _getThemeData(BuildContext context) {
+ if (Platform.isAndroid) {
+ return Theme.of(context);
+ }
+
+ // hide the splash effect for iOS
+ return Theme.of(context).copyWith(
+ splashFactory: NoSplash.splashFactory,
+ splashColor: Colors.transparent,
+ highlightColor: Colors.transparent,
+ );
+ }
+
/// Navigate to the current location of the branch at the provided index when
/// tapping an item in the BottomNavigationBar.
void _onTap(BuildContext context, int bottomBarIndex) {
- if (_items[bottomBarIndex].label == _addLabel) {
+ // close the popup menu
+ closePopupMenu();
+
+ final label = _items[bottomBarIndex].label;
+ if (label == _addLabel) {
// show an add dialog
- createNewPageNotifier.value = ViewLayoutPB.Document;
+ mobileCreateNewPageNotifier.value = ViewLayoutPB.Document;
return;
+ } else if (label == _notificationLabel) {
+ getIt().add(const ReminderEvent.refresh());
}
// When navigating to a new branch, it's recommended to use the goBranch
// method, as doing so makes sure the last navigation state of the
@@ -106,3 +254,112 @@ class MobileBottomNavigationBar extends StatelessWidget {
);
}
}
+
+class _NotificationNavigationBar extends StatelessWidget {
+ const _NotificationNavigationBar();
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ // todo: use real height here.
+ height: 90,
+ decoration: BoxDecoration(
+ border: context.border,
+ color: context.backgroundColor,
+ ),
+ padding: const EdgeInsets.only(bottom: 20),
+ child: ValueListenableBuilder(
+ valueListenable: mSelectedNotificationIds,
+ builder: (context, value, child) {
+ if (value.isEmpty) {
+ // not editable
+ return IgnorePointer(
+ child: Opacity(
+ opacity: 0.3,
+ child: child,
+ ),
+ );
+ }
+
+ return child!;
+ },
+ child: Row(
+ children: [
+ const HSpace(20),
+ Expanded(
+ child: NavigationBarButton(
+ icon: FlowySvgs.m_notification_action_mark_as_read_s,
+ text: LocaleKeys.settings_notifications_action_markAsRead.tr(),
+ onTap: () => _onMarkAsRead(context),
+ ),
+ ),
+ const HSpace(16),
+ Expanded(
+ child: NavigationBarButton(
+ icon: FlowySvgs.m_notification_action_archive_s,
+ text: LocaleKeys.settings_notifications_action_archive.tr(),
+ onTap: () => _onArchive(context),
+ ),
+ ),
+ const HSpace(20),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _onMarkAsRead(BuildContext context) {
+ if (mSelectedNotificationIds.value.isEmpty) {
+ return;
+ }
+
+ showToastNotification(
+ context,
+ message: LocaleKeys
+ .settings_notifications_markAsReadNotifications_allSuccess
+ .tr(),
+ );
+
+ getIt()
+ .add(ReminderEvent.markAsRead(mSelectedNotificationIds.value));
+
+ mSelectedNotificationIds.value = [];
+ }
+
+ void _onArchive(BuildContext context) {
+ if (mSelectedNotificationIds.value.isEmpty) {
+ return;
+ }
+
+ showToastNotification(
+ context,
+ message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
+ .tr(),
+ );
+
+ getIt()
+ .add(ReminderEvent.archive(mSelectedNotificationIds.value));
+
+ mSelectedNotificationIds.value = [];
+ }
+}
+
+extension on BuildContext {
+ Color get backgroundColor {
+ return Theme.of(this).isLightMode
+ ? Colors.white.withOpacity(0.95)
+ : const Color(0xFF23262B).withOpacity(0.95);
+ }
+
+ Color get borderColor {
+ return Theme.of(this).isLightMode
+ ? const Color(0x141F2329)
+ : const Color(0xFF23262B).withOpacity(0.5);
+ }
+
+ Border? get border {
+ return Theme.of(this).isLightMode
+ ? Border(top: BorderSide(color: borderColor))
+ : null;
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart
new file mode 100644
index 0000000000000..cf7ce35e802ba
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart
@@ -0,0 +1,59 @@
+import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
+import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MobileNotificationsMultiSelectScreen extends StatelessWidget {
+ const MobileNotificationsMultiSelectScreen({super.key});
+
+ static const routeName = '/notifications_multi_select';
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocProvider.value(
+ value: getIt(),
+ child: const MobileNotificationMultiSelect(),
+ );
+ }
+}
+
+class MobileNotificationMultiSelect extends StatefulWidget {
+ const MobileNotificationMultiSelect({
+ super.key,
+ });
+
+ @override
+ State createState() =>
+ _MobileNotificationMultiSelectState();
+}
+
+class _MobileNotificationMultiSelectState
+ extends State {
+ @override
+ void dispose() {
+ mSelectedNotificationIds.value.clear();
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return const Scaffold(
+ body: SafeArea(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ MobileNotificationMultiSelectPageHeader(),
+ VSpace(12.0),
+ Expanded(
+ child: MultiSelectNotificationTab(),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_screen.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_screen.dart
new file mode 100644
index 0000000000000..b1219d8e9874c
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/mobile_notifications_screen.dart
@@ -0,0 +1,129 @@
+import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_multiple_select_page.dart';
+import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
+import 'package:appflowy/mobile/presentation/presentation.dart';
+import 'package:appflowy/startup/startup.dart';
+import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+final PropertyValueNotifier> mSelectedNotificationIds =
+ PropertyValueNotifier([]);
+
+class MobileNotificationsScreenV2 extends StatefulWidget {
+ const MobileNotificationsScreenV2({super.key});
+
+ static const routeName = '/notifications';
+
+ @override
+ State createState() =>
+ _MobileNotificationsScreenV2State();
+}
+
+class _MobileNotificationsScreenV2State
+ extends State
+ with AutomaticKeepAliveClientMixin {
+ @override
+ bool get wantKeepAlive => true;
+
+ @override
+ void initState() {
+ super.initState();
+
+ mCurrentWorkspace.addListener(_onRefresh);
+ }
+
+ @override
+ void dispose() {
+ mCurrentWorkspace.removeListener(_onRefresh);
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ super.build(context);
+
+ return BlocProvider.value(
+ value: getIt(),
+ child: ValueListenableBuilder(
+ valueListenable: bottomNavigationBarType,
+ builder: (_, value, __) {
+ switch (value) {
+ case BottomNavigationBarActionType.home:
+ return const MobileNotificationsTab();
+ case BottomNavigationBarActionType.notificationMultiSelect:
+ return const MobileNotificationMultiSelect();
+ }
+ },
+ ),
+ );
+ }
+
+ void _onRefresh() {
+ getIt().add(const ReminderEvent.refresh());
+ }
+}
+
+class MobileNotificationsTab extends StatefulWidget {
+ const MobileNotificationsTab({
+ super.key,
+ });
+
+ @override
+ State createState() => _MobileNotificationsTabState();
+}
+
+class _MobileNotificationsTabState extends State
+ with SingleTickerProviderStateMixin {
+ late TabController tabController;
+
+ final tabs = [
+ MobileNotificationTabType.inbox,
+ MobileNotificationTabType.unread,
+ MobileNotificationTabType.archive,
+ ];
+
+ @override
+ void initState() {
+ super.initState();
+
+ tabController = TabController(
+ length: 3,
+ vsync: this,
+ );
+ }
+
+ @override
+ void dispose() {
+ tabController.dispose();
+
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ body: SafeArea(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const MobileNotificationPageHeader(),
+ MobileNotificationTabBar(
+ tabController: tabController,
+ tabs: tabs,
+ ),
+ const VSpace(12.0),
+ Expanded(
+ child: TabBarView(
+ controller: tabController,
+ children: tabs.map((e) => NotificationTab(tabType: e)).toList(),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart
new file mode 100644
index 0000000000000..8a59336378028
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/color.dart
@@ -0,0 +1,11 @@
+import 'package:appflowy/util/theme_extension.dart';
+import 'package:flutter/material.dart';
+
+extension NotificationItemColors on BuildContext {
+ Color get notificationItemTextColor {
+ if (Theme.of(this).isLightMode) {
+ return const Color(0xFF171717);
+ }
+ return const Color(0xFFffffff).withOpacity(0.8);
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/empty.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/empty.dart
new file mode 100644
index 0000000000000..e5598cc6e591a
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/empty.dart
@@ -0,0 +1,58 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+
+class EmptyNotification extends StatelessWidget {
+ const EmptyNotification({
+ super.key,
+ required this.type,
+ });
+
+ final MobileNotificationTabType type;
+
+ @override
+ Widget build(BuildContext context) {
+ final title = switch (type) {
+ MobileNotificationTabType.inbox =>
+ LocaleKeys.settings_notifications_emptyInbox_title.tr(),
+ MobileNotificationTabType.archive =>
+ LocaleKeys.settings_notifications_emptyArchived_title.tr(),
+ MobileNotificationTabType.unread =>
+ LocaleKeys.settings_notifications_emptyUnread_title.tr(),
+ };
+ final desc = switch (type) {
+ MobileNotificationTabType.inbox =>
+ LocaleKeys.settings_notifications_emptyInbox_description.tr(),
+ MobileNotificationTabType.archive =>
+ LocaleKeys.settings_notifications_emptyArchived_description.tr(),
+ MobileNotificationTabType.unread =>
+ LocaleKeys.settings_notifications_emptyUnread_description.tr(),
+ };
+ return Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const FlowySvg(FlowySvgs.m_empty_notification_xl),
+ const VSpace(12.0),
+ FlowyText(
+ title,
+ fontSize: 16.0,
+ figmaLineHeight: 24.0,
+ fontWeight: FontWeight.w500,
+ ),
+ const VSpace(4.0),
+ Opacity(
+ opacity: 0.45,
+ child: FlowyText(
+ desc,
+ fontSize: 15.0,
+ figmaLineHeight: 22.0,
+ fontWeight: FontWeight.w400,
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/header.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/header.dart
new file mode 100644
index 0000000000000..9f1311d1e714f
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/header.dart
@@ -0,0 +1,96 @@
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
+import 'package:appflowy/mobile/presentation/notifications/widgets/settings_popup_menu.dart';
+import 'package:appflowy/mobile/presentation/presentation.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+
+class MobileNotificationPageHeader extends StatelessWidget {
+ const MobileNotificationPageHeader({
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 56),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const HSpace(18.0),
+ FlowyText(
+ LocaleKeys.settings_notifications_titles_notifications.tr(),
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ ),
+ const Spacer(),
+ const NotificationSettingsPopupMenu(),
+ const HSpace(16.0),
+ ],
+ ),
+ );
+ }
+}
+
+class MobileNotificationMultiSelectPageHeader extends StatelessWidget {
+ const MobileNotificationMultiSelectPageHeader({
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return ConstrainedBox(
+ constraints: const BoxConstraints(minHeight: 56),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ _buildCancelButton(
+ isOpaque: false,
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ onTap: () => bottomNavigationBarType.value =
+ BottomNavigationBarActionType.home,
+ ),
+ ValueListenableBuilder(
+ valueListenable: mSelectedNotificationIds,
+ builder: (_, value, __) {
+ return FlowyText(
+ // todo: i18n
+ '${value.length} Selected',
+ fontSize: 17.0,
+ figmaLineHeight: 24.0,
+ fontWeight: FontWeight.w500,
+ );
+ },
+ ),
+ // this button is used to align the text to the center
+ _buildCancelButton(
+ isOpaque: true,
+ padding: const EdgeInsets.symmetric(horizontal: 16),
+ ),
+ ],
+ ),
+ );
+ }
+
+ //
+ Widget _buildCancelButton({
+ required bool isOpaque,
+ required EdgeInsets padding,
+ VoidCallback? onTap,
+ }) {
+ return GestureDetector(
+ onTap: onTap,
+ child: Padding(
+ padding: padding,
+ child: FlowyText(
+ LocaleKeys.button_cancel.tr(),
+ fontSize: 17.0,
+ figmaLineHeight: 24.0,
+ fontWeight: FontWeight.w400,
+ color: isOpaque ? Colors.transparent : null,
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart
new file mode 100644
index 0000000000000..ea57d5d391c23
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/multi_select_notification_item.dart
@@ -0,0 +1,115 @@
+import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
+import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
+import 'package:appflowy/mobile/presentation/notifications/mobile_notifications_screen.dart';
+import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+class MultiSelectNotificationItem extends StatelessWidget {
+ const MultiSelectNotificationItem({
+ super.key,
+ required this.reminder,
+ });
+
+ final ReminderPB reminder;
+
+ @override
+ Widget build(BuildContext context) {
+ final settings = context.read().state;
+ final dateFormate = settings.dateFormat;
+ final timeFormate = settings.timeFormat;
+ return BlocProvider(
+ create: (context) => NotificationReminderBloc()
+ ..add(
+ NotificationReminderEvent.initial(
+ reminder,
+ dateFormate,
+ timeFormate,
+ ),
+ ),
+ child: BlocBuilder(
+ builder: (context, state) {
+ if (state.status == NotificationReminderStatus.loading ||
+ state.status == NotificationReminderStatus.initial) {
+ return const SizedBox.shrink();
+ }
+
+ if (state.status == NotificationReminderStatus.error) {
+ // error handle.
+ return const SizedBox.shrink();
+ }
+
+ final child = ValueListenableBuilder(
+ valueListenable: mSelectedNotificationIds,
+ builder: (_, selectedIds, child) {
+ return Container(
+ margin: const EdgeInsets.symmetric(horizontal: 12),
+ decoration: selectedIds.contains(reminder.id)
+ ? ShapeDecoration(
+ color: const Color(0x1900BCF0),
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(10),
+ ),
+ )
+ : null,
+ child: child,
+ );
+ },
+ child: Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: _InnerNotificationItem(
+ reminder: reminder,
+ ),
+ ),
+ );
+
+ return AnimatedGestureDetector(
+ scaleFactor: 0.99,
+ onTapUp: () {
+ if (mSelectedNotificationIds.value.contains(reminder.id)) {
+ mSelectedNotificationIds.value = mSelectedNotificationIds.value
+ ..remove(reminder.id);
+ } else {
+ mSelectedNotificationIds.value = mSelectedNotificationIds.value
+ ..add(reminder.id);
+ }
+ },
+ child: child,
+ );
+ },
+ ),
+ );
+ }
+}
+
+class _InnerNotificationItem extends StatelessWidget {
+ const _InnerNotificationItem({
+ required this.reminder,
+ });
+
+ final ReminderPB reminder;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const HSpace(10.0),
+ NotificationCheckIcon(
+ isSelected: mSelectedNotificationIds.value.contains(reminder.id),
+ ),
+ const HSpace(3.0),
+ !reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
+ const HSpace(3.0),
+ NotificationIcon(reminder: reminder),
+ const HSpace(12.0),
+ Expanded(
+ child: NotificationContent(reminder: reminder),
+ ),
+ ],
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart
new file mode 100644
index 0000000000000..8f6db18265da5
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/notification_item.dart
@@ -0,0 +1,164 @@
+import 'package:appflowy/mobile/application/mobile_router.dart';
+import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
+import 'package:appflowy/mobile/presentation/base/animated_gesture.dart';
+import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
+import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
+import 'package:appflowy/workspace/application/settings/appearance/appearance_cubit.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+import 'package:flutter_slidable/flutter_slidable.dart';
+
+class NotificationItem extends StatelessWidget {
+ const NotificationItem({
+ super.key,
+ required this.tabType,
+ required this.reminder,
+ });
+
+ final MobileNotificationTabType tabType;
+ final ReminderPB reminder;
+
+ @override
+ Widget build(BuildContext context) {
+ final settings = context.read().state;
+ final dateFormate = settings.dateFormat;
+ final timeFormate = settings.timeFormat;
+ return BlocProvider(
+ create: (context) => NotificationReminderBloc()
+ ..add(
+ NotificationReminderEvent.initial(
+ reminder,
+ dateFormate,
+ timeFormate,
+ ),
+ ),
+ child: BlocBuilder(
+ builder: (context, state) {
+ if (state.status == NotificationReminderStatus.loading ||
+ state.status == NotificationReminderStatus.initial) {
+ return const SizedBox.shrink();
+ }
+
+ if (state.status == NotificationReminderStatus.error) {
+ // error handle.
+ return const SizedBox.shrink();
+ }
+
+ final child = Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8),
+ child: _SlidableNotificationItem(
+ tabType: tabType,
+ reminder: reminder,
+ child: _InnerNotificationItem(
+ tabType: tabType,
+ reminder: reminder,
+ ),
+ ),
+ );
+
+ return AnimatedGestureDetector(
+ scaleFactor: 0.99,
+ child: child,
+ onTapUp: () async {
+ final view = state.view;
+ if (view == null) {
+ return;
+ }
+
+ await context.pushView(view);
+
+ if (!reminder.isRead && context.mounted) {
+ context.read().add(
+ ReminderEvent.markAsRead([reminder.id]),
+ );
+ }
+ },
+ );
+ },
+ ),
+ );
+ }
+}
+
+class _InnerNotificationItem extends StatelessWidget {
+ const _InnerNotificationItem({
+ required this.reminder,
+ required this.tabType,
+ });
+
+ final MobileNotificationTabType tabType;
+ final ReminderPB reminder;
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const HSpace(8.0),
+ !reminder.isRead ? const UnreadRedDot() : const HSpace(6.0),
+ const HSpace(4.0),
+ NotificationIcon(reminder: reminder),
+ const HSpace(12.0),
+ Expanded(
+ child: NotificationContent(reminder: reminder),
+ ),
+ ],
+ );
+ }
+}
+
+class _SlidableNotificationItem extends StatelessWidget {
+ const _SlidableNotificationItem({
+ required this.tabType,
+ required this.reminder,
+ required this.child,
+ });
+
+ final MobileNotificationTabType tabType;
+ final ReminderPB reminder;
+ final Widget child;
+
+ @override
+ Widget build(BuildContext context) {
+ final List actions = switch (tabType) {
+ MobileNotificationTabType.inbox => [
+ NotificationPaneActionType.more,
+ if (!reminder.isRead) NotificationPaneActionType.markAsRead,
+ ],
+ MobileNotificationTabType.unread => [
+ NotificationPaneActionType.more,
+ NotificationPaneActionType.markAsRead,
+ ],
+ MobileNotificationTabType.archive => [
+ if (kDebugMode) NotificationPaneActionType.unArchive,
+ ],
+ };
+
+ if (actions.isEmpty) {
+ return child;
+ }
+
+ final children = actions
+ .map(
+ (action) => action.actionButton(
+ context,
+ tabType: tabType,
+ ),
+ )
+ .toList();
+
+ final extentRatio = actions.length == 1 ? 1 / 5 : 1 / 3;
+
+ return Slidable(
+ endActionPane: ActionPane(
+ motion: const ScrollMotion(),
+ extentRatio: extentRatio,
+ children: children,
+ ),
+ child: child,
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart
new file mode 100644
index 0000000000000..dfa277f2ef33d
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/settings_popup_menu.dart
@@ -0,0 +1,170 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/presentation/presentation.dart';
+import 'package:appflowy/shared/popup_menu/appflowy_popup_menu.dart';
+import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flowy_infra_ui/flowy_infra_ui.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart'
+ hide PopupMenuButton, PopupMenuDivider, PopupMenuItem, PopupMenuEntry;
+import 'package:go_router/go_router.dart';
+import 'package:provider/provider.dart';
+
+enum _NotificationSettingsPopupMenuItem {
+ settings,
+ markAllAsRead,
+ archiveAll,
+ // only visible in debug mode
+ unarchiveAll;
+}
+
+class NotificationSettingsPopupMenu extends StatelessWidget {
+ const NotificationSettingsPopupMenu({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return PopupMenuButton<_NotificationSettingsPopupMenuItem>(
+ offset: const Offset(0, 36),
+ padding: EdgeInsets.zero,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(
+ Radius.circular(12.0),
+ ),
+ ),
+ // todo: replace it with shadows
+ shadowColor: const Color(0x68000000),
+ elevation: 10,
+ color: context.popupMenuBackgroundColor,
+ itemBuilder: (BuildContext context) =>
+ >[
+ _buildItem(
+ value: _NotificationSettingsPopupMenuItem.settings,
+ svg: FlowySvgs.m_notification_settings_s,
+ text: LocaleKeys.settings_notifications_settings_settings.tr(),
+ ),
+ const PopupMenuDivider(height: 0.5),
+ _buildItem(
+ value: _NotificationSettingsPopupMenuItem.markAllAsRead,
+ svg: FlowySvgs.m_notification_mark_as_read_s,
+ text: LocaleKeys.settings_notifications_settings_markAllAsRead.tr(),
+ ),
+ const PopupMenuDivider(height: 0.5),
+ _buildItem(
+ value: _NotificationSettingsPopupMenuItem.archiveAll,
+ svg: FlowySvgs.m_notification_archived_s,
+ text: LocaleKeys.settings_notifications_settings_archiveAll.tr(),
+ ),
+ // only visible in debug mode
+ if (kDebugMode) ...[
+ const PopupMenuDivider(height: 0.5),
+ _buildItem(
+ value: _NotificationSettingsPopupMenuItem.unarchiveAll,
+ svg: FlowySvgs.m_notification_archived_s,
+ text: 'Unarchive all (Debug Mode)',
+ ),
+ ],
+ ],
+ onSelected: (_NotificationSettingsPopupMenuItem value) {
+ switch (value) {
+ case _NotificationSettingsPopupMenuItem.markAllAsRead:
+ _onMarkAllAsRead(context);
+ break;
+ case _NotificationSettingsPopupMenuItem.archiveAll:
+ _onArchiveAll(context);
+ break;
+ case _NotificationSettingsPopupMenuItem.settings:
+ context.push(MobileHomeSettingPage.routeName);
+ break;
+ case _NotificationSettingsPopupMenuItem.unarchiveAll:
+ _onUnarchiveAll(context);
+ break;
+ }
+ },
+ child: const Padding(
+ padding: EdgeInsets.all(8.0),
+ child: FlowySvg(
+ FlowySvgs.m_settings_more_s,
+ ),
+ ),
+ );
+ }
+
+ PopupMenuItem _buildItem({
+ required T value,
+ required FlowySvgData svg,
+ required String text,
+ }) {
+ return PopupMenuItem(
+ value: value,
+ padding: EdgeInsets.zero,
+ child: _PopupButton(
+ svg: svg,
+ text: text,
+ ),
+ );
+ }
+
+ void _onMarkAllAsRead(BuildContext context) {
+ showToastNotification(
+ context,
+ message: LocaleKeys
+ .settings_notifications_markAsReadNotifications_allSuccess
+ .tr(),
+ );
+
+ context.read().add(const ReminderEvent.markAllRead());
+ }
+
+ void _onArchiveAll(BuildContext context) {
+ showToastNotification(
+ context,
+ message: LocaleKeys.settings_notifications_archiveNotifications_allSuccess
+ .tr(),
+ );
+
+ context.read().add(const ReminderEvent.archiveAll());
+ }
+
+ void _onUnarchiveAll(BuildContext context) {
+ if (!kDebugMode) {
+ return;
+ }
+
+ showToastNotification(
+ context,
+ message: 'Unarchive all success (Debug Mode)',
+ );
+
+ context.read().add(const ReminderEvent.unarchiveAll());
+ }
+}
+
+class _PopupButton extends StatelessWidget {
+ const _PopupButton({
+ required this.svg,
+ required this.text,
+ });
+
+ final FlowySvgData svg;
+ final String text;
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ height: 44,
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ child: Row(
+ children: [
+ FlowySvg(svg),
+ const HSpace(12),
+ FlowyText.regular(
+ text,
+ fontSize: 16,
+ ),
+ ],
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/shared.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/shared.dart
new file mode 100644
index 0000000000000..4f1a7d2db051e
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/shared.dart
@@ -0,0 +1,286 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
+import 'package:appflowy/mobile/application/page_style/document_page_style_bloc.dart';
+import 'package:appflowy/mobile/presentation/notifications/widgets/color.dart';
+import 'package:appflowy/plugins/document/presentation/editor_configuration.dart';
+import 'package:appflowy/plugins/document/presentation/editor_style.dart';
+import 'package:appflowy/user/application/reminder/reminder_extension.dart';
+import 'package:appflowy/workspace/application/view/view_ext.dart';
+import 'package:appflowy_backend/protobuf/flowy-folder/view.pb.dart';
+import 'package:appflowy_backend/protobuf/flowy-user/protobuf.dart';
+import 'package:appflowy_editor/appflowy_editor.dart';
+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';
+
+const _kNotificationIconHeight = 36.0;
+
+class NotificationIcon extends StatelessWidget {
+ const NotificationIcon({
+ super.key,
+ required this.reminder,
+ });
+
+ final ReminderPB reminder;
+
+ @override
+ Widget build(BuildContext context) {
+ return const FlowySvg(
+ FlowySvgs.m_notification_reminder_s,
+ size: Size.square(_kNotificationIconHeight),
+ blendMode: null,
+ );
+ }
+}
+
+class NotificationCheckIcon extends StatelessWidget {
+ const NotificationCheckIcon({super.key, required this.isSelected});
+
+ final bool isSelected;
+
+ @override
+ Widget build(BuildContext context) {
+ return SizedBox(
+ height: _kNotificationIconHeight,
+ child: Center(
+ child: FlowySvg(
+ isSelected
+ ? FlowySvgs.m_notification_multi_select_s
+ : FlowySvgs.m_notification_multi_unselect_s,
+ blendMode: isSelected ? null : BlendMode.srcIn,
+ ),
+ ),
+ );
+ }
+}
+
+class UnreadRedDot extends StatelessWidget {
+ const UnreadRedDot({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const SizedBox(
+ height: _kNotificationIconHeight,
+ child: Center(
+ child: SizedBox.square(
+ dimension: 6.0,
+ child: DecoratedBox(
+ decoration: ShapeDecoration(
+ color: Color(0xFFFF6331),
+ shape: OvalBorder(),
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+}
+
+class NotificationContent extends StatefulWidget {
+ const NotificationContent({
+ super.key,
+ required this.reminder,
+ });
+
+ final ReminderPB reminder;
+
+ @override
+ State createState() => _NotificationContentState();
+}
+
+class _NotificationContentState extends State {
+ @override
+ void didUpdateWidget(covariant NotificationContent oldWidget) {
+ super.didUpdateWidget(oldWidget);
+
+ context.read().add(
+ const NotificationReminderEvent.reset(),
+ );
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return BlocBuilder(
+ builder: (context, state) {
+ final view = state.view;
+ if (view == null) {
+ return const SizedBox.shrink();
+ }
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // title
+ _buildHeader(),
+
+ // time & page name
+ _buildTimeAndPageName(
+ context,
+ state.createdAt,
+ state.pageTitle,
+ ),
+
+ // content
+ Padding(
+ padding: const EdgeInsets.only(right: 16.0),
+ child: _buildContent(view, nodes: state.nodes),
+ ),
+ ],
+ );
+ },
+ );
+ }
+
+ Widget _buildContent(ViewPB view, {List? nodes}) {
+ if (view.layout.isDocumentView && nodes != null) {
+ return IntrinsicHeight(
+ child: BlocProvider(
+ create: (context) => DocumentPageStyleBloc(view: view),
+ child: NotificationDocumentContent(
+ reminder: widget.reminder,
+ nodes: nodes,
+ ),
+ ),
+ );
+ } else if (view.layout.isDatabaseView) {
+ final opacity = widget.reminder.type == ReminderType.past ? 0.3 : 1.0;
+ return Opacity(
+ opacity: opacity,
+ child: FlowyText(
+ widget.reminder.message,
+ fontSize: 14,
+ figmaLineHeight: 22,
+ color: context.notificationItemTextColor,
+ ),
+ );
+ }
+
+ return const SizedBox.shrink();
+ }
+
+ Widget _buildHeader() {
+ return FlowyText.semibold(
+ LocaleKeys.settings_notifications_titles_reminder.tr(),
+ fontSize: 14,
+ figmaLineHeight: 20,
+ );
+ }
+
+ Widget _buildTimeAndPageName(
+ BuildContext context,
+ String createdAt,
+ String pageTitle,
+ ) {
+ return Opacity(
+ opacity: 0.5,
+ child: Row(
+ children: [
+ // the legacy reminder doesn't contain the timestamp, so we don't show it
+ if (createdAt.isNotEmpty) ...[
+ FlowyText.regular(
+ createdAt,
+ fontSize: 12,
+ figmaLineHeight: 18,
+ color: context.notificationItemTextColor,
+ ),
+ const NotificationEllipse(),
+ ],
+ FlowyText.regular(
+ pageTitle,
+ fontSize: 12,
+ figmaLineHeight: 18,
+ color: context.notificationItemTextColor,
+ ),
+ ],
+ ),
+ );
+ }
+}
+
+class NotificationEllipse extends StatelessWidget {
+ const NotificationEllipse({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ width: 2.50,
+ height: 2.50,
+ margin: const EdgeInsets.symmetric(horizontal: 5.0),
+ decoration: ShapeDecoration(
+ color: context.notificationItemTextColor,
+ shape: const OvalBorder(),
+ ),
+ );
+ }
+}
+
+class NotificationDocumentContent extends StatelessWidget {
+ const NotificationDocumentContent({
+ super.key,
+ required this.reminder,
+ required this.nodes,
+ });
+
+ final ReminderPB reminder;
+ final List nodes;
+
+ @override
+ Widget build(BuildContext context) {
+ final editorState = EditorState(
+ document: Document(
+ root: pageNode(children: nodes),
+ ),
+ );
+
+ final styleCustomizer = EditorStyleCustomizer(
+ context: context,
+ padding: EdgeInsets.zero,
+ );
+
+ final editorStyle = styleCustomizer.style().copyWith(
+ // hide the cursor
+ cursorColor: Colors.transparent,
+ cursorWidth: 0,
+ textStyleConfiguration: TextStyleConfiguration(
+ lineHeight: 22 / 14,
+ applyHeightToFirstAscent: true,
+ applyHeightToLastDescent: true,
+ text: TextStyle(
+ fontSize: 14,
+ color: context.notificationItemTextColor,
+ height: 22 / 14,
+ fontWeight: FontWeight.w400,
+ leadingDistribution: TextLeadingDistribution.even,
+ ),
+ ),
+ );
+
+ final blockBuilders = getEditorBuilderMap(
+ context: context,
+ editorState: editorState,
+ styleCustomizer: styleCustomizer,
+ // the editor is not editable in the chat
+ editable: false,
+ customHeadingPadding: EdgeInsets.zero,
+ );
+
+ return IgnorePointer(
+ child: Opacity(
+ opacity: reminder.type == ReminderType.past ? 0.3 : 1,
+ child: AppFlowyEditor(
+ editorState: editorState,
+ editorStyle: editorStyle,
+ disableSelectionService: true,
+ disableKeyboardService: true,
+ disableScrollService: true,
+ editable: false,
+ shrinkWrap: true,
+ blockComponentBuilders: blockBuilders,
+ ),
+ ),
+ );
+ }
+}
diff --git a/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart
new file mode 100644
index 0000000000000..85f468c76c1ee
--- /dev/null
+++ b/frontend/appflowy_flutter/lib/mobile/presentation/notifications/widgets/slide_actions.dart
@@ -0,0 +1,212 @@
+import 'package:appflowy/generated/flowy_svgs.g.dart';
+import 'package:appflowy/generated/locale_keys.g.dart';
+import 'package:appflowy/mobile/application/notification/notification_reminder_bloc.dart';
+import 'package:appflowy/mobile/presentation/bottom_sheet/bottom_sheet.dart';
+import 'package:appflowy/mobile/presentation/notifications/widgets/widgets.dart';
+import 'package:appflowy/mobile/presentation/page_item/mobile_slide_action_button.dart';
+import 'package:appflowy/mobile/presentation/presentation.dart';
+import 'package:appflowy/mobile/presentation/widgets/widgets.dart';
+import 'package:appflowy/user/application/reminder/reminder_bloc.dart';
+import 'package:appflowy/user/application/reminder/reminder_extension.dart';
+import 'package:appflowy/workspace/presentation/widgets/dialogs.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_bloc/flutter_bloc.dart';
+
+enum NotificationPaneActionType {
+ more,
+ markAsRead,
+ // only used in the debug mode.
+ unArchive;
+
+ MobileSlideActionButton actionButton(
+ BuildContext context, {
+ required MobileNotificationTabType tabType,
+ }) {
+ switch (this) {
+ case NotificationPaneActionType.markAsRead:
+ return MobileSlideActionButton(
+ backgroundColor: const Color(0xFF00C8FF),
+ svg: FlowySvgs.m_notification_action_mark_as_read_s,
+ size: 24.0,
+ onPressed: (context) {
+ showToastNotification(
+ context,
+ message: LocaleKeys
+ .settings_notifications_markAsReadNotifications_success
+ .tr(),
+ );
+
+ context.read().add(
+ ReminderEvent.update(
+ ReminderUpdate(
+ id: context.read